[ARVADOS] created: 1.3.0-1240-g2a5eef36c
Git user
git at public.curoverse.com
Tue Jul 2 20:33:42 UTC 2019
at 2a5eef36c4a863e72d2bd921ba0d129b6924986e (commit)
commit 2a5eef36c4a863e72d2bd921ba0d129b6924986e
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date: Tue Jul 2 16:32:02 2019 -0400
14287: Merge list results from multiple backends.
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>
diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
index e094953fc..aa2481c1f 100644
--- a/lib/controller/federation/conn.go
+++ b/lib/controller/federation/conn.go
@@ -27,7 +27,7 @@ type Conn struct {
remotes map[string]backend
}
-func New(cluster *arvados.Cluster) arvados.API {
+func New(cluster *arvados.Cluster) *Conn {
local := railsproxy.NewConn(cluster)
remotes := map[string]backend{}
for id, remote := range cluster.RemoteClusters {
@@ -218,10 +218,6 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions)
}
}
-func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
- return conn.local.CollectionList(ctx, options)
-}
-
func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options)
}
@@ -254,10 +250,6 @@ func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions)
return conn.chooseBackend(options.UUID).ContainerGet(ctx, options)
}
-func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
- return conn.local.ContainerList(ctx, options)
-}
-
func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
return conn.chooseBackend(options.UUID).ContainerDelete(ctx, options)
}
@@ -282,14 +274,6 @@ func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (
return conn.chooseBackend(options.UUID).SpecimenGet(ctx, options)
}
-func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
- return conn.local.SpecimenList(ctx, options)
-}
-
-func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
- return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
-}
-
func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
}
diff --git a/lib/controller/federation/generate.go b/lib/controller/federation/generate.go
new file mode 100644
index 000000000..1f37df858
--- /dev/null
+++ b/lib/controller/federation/generate.go
@@ -0,0 +1,95 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// +build ignore
+
+package main
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "regexp"
+)
+
+func main() {
+ checkOnly := false
+ if len(os.Args) == 2 && os.Args[1] == "-check" {
+ checkOnly = true
+ } else if len(os.Args) != 1 {
+ panic("usage: go run generate.go [-check]")
+ }
+
+ in, err := os.Open("list.go")
+ if err != nil {
+ panic(err)
+ }
+ buf, err := ioutil.ReadAll(in)
+ if err != nil {
+ panic(err)
+ }
+ orig := regexp.MustCompile(`(?ms)\nfunc [^\n]*CollectionList\(.*?\n}\n`).Find(buf)
+ if len(orig) == 0 {
+ panic("can't find CollectionList func")
+ }
+
+ outfile, err := os.OpenFile("generated.go~", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
+ if err != nil {
+ panic(err)
+ }
+
+ gofmt := exec.Command("goimports")
+ gofmt.Stdout = outfile
+ gofmt.Stderr = os.Stderr
+ out, err := gofmt.StdinPipe()
+ if err != nil {
+ panic(err)
+ }
+ go func() {
+ out.Write(regexp.MustCompile(`(?ms)^.*package .*?import.*?\n\)\n`).Find(buf))
+ io.WriteString(out, "//\n// -- this file is auto-generated -- do not edit -- edit list.go and run \"go generate\" instead --\n//\n\n")
+ for _, t := range []string{"Container", "Specimen"} {
+ _, err := out.Write(bytes.ReplaceAll(orig, []byte("Collection"), []byte(t)))
+ if err != nil {
+ panic(err)
+ }
+ }
+ err = out.Close()
+ if err != nil {
+ panic(err)
+ }
+ }()
+ err = gofmt.Run()
+ if err != nil {
+ panic(err)
+ }
+ err = outfile.Close()
+ if err != nil {
+ panic(err)
+ }
+ if checkOnly {
+ diff := exec.Command("diff", "-u", "/dev/fd/3", "/dev/fd/4")
+ for _, fnm := range []string{"generated.go", "generated.go~"} {
+ f, err := os.Open(fnm)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ diff.ExtraFiles = append(diff.ExtraFiles, f)
+ }
+ diff.Stdout = os.Stdout
+ diff.Stderr = os.Stderr
+ err = diff.Run()
+ if err != nil {
+ os.Exit(1)
+ }
+ } else {
+ err = os.Rename("generated.go~", "generated.go")
+ if err != nil {
+ panic(err)
+ }
+ }
+}
diff --git a/lib/controller/federation/generated.go b/lib/controller/federation/generated.go
new file mode 100755
index 000000000..b34b9b165
--- /dev/null
+++ b/lib/controller/federation/generated.go
@@ -0,0 +1,67 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+ "context"
+ "sort"
+ "sync"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+//
+// -- this file is auto-generated -- do not edit -- edit list.go and run "go generate" instead --
+//
+
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+ var mtx sync.Mutex
+ var merged arvados.ContainerList
+ err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+ cl, err := backend.ContainerList(ctx, options)
+ if err != nil {
+ return nil, err
+ }
+ mtx.Lock()
+ defer mtx.Unlock()
+ if len(merged.Items) == 0 {
+ merged = cl
+ } else {
+ merged.Items = append(merged.Items, cl.Items...)
+ }
+ uuids := make([]string, 0, len(cl.Items))
+ for _, item := range cl.Items {
+ uuids = append(uuids, item.UUID)
+ }
+ return uuids, nil
+ })
+ sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+ return merged, err
+}
+
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+ var mtx sync.Mutex
+ var merged arvados.SpecimenList
+ err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+ cl, err := backend.SpecimenList(ctx, options)
+ if err != nil {
+ return nil, err
+ }
+ mtx.Lock()
+ defer mtx.Unlock()
+ if len(merged.Items) == 0 {
+ merged = cl
+ } else {
+ merged.Items = append(merged.Items, cl.Items...)
+ }
+ uuids := make([]string, 0, len(cl.Items))
+ for _, item := range cl.Items {
+ uuids = append(uuids, item.UUID)
+ }
+ return uuids, nil
+ })
+ sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+ return merged, err
+}
diff --git a/lib/controller/federation/generated_test.go b/lib/controller/federation/generated_test.go
new file mode 100644
index 000000000..0e571f2fa
--- /dev/null
+++ b/lib/controller/federation/generated_test.go
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+ "os/exec"
+
+ check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&UptodateSuite{})
+
+type UptodateSuite struct{}
+
+func (*UptodateSuite) TestUpToDate(c *check.C) {
+ output, err := exec.Command("go", "run", "generate.go", "-check").CombinedOutput()
+ if err != nil {
+ c.Log(string(output))
+ c.Error("generated.go is out of date -- run 'go generate' to update it")
+ }
+}
diff --git a/lib/controller/federation/list.go b/lib/controller/federation/list.go
new file mode 100644
index 000000000..5a171c9c3
--- /dev/null
+++ b/lib/controller/federation/list.go
@@ -0,0 +1,247 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "sort"
+ "sync"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/httpserver"
+)
+
+//go:generate go run generate.go
+
+// CollectionList is used as a template to auto-generate List()
+// methods for other types; see generate.go.
+
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+ var mtx sync.Mutex
+ var merged arvados.CollectionList
+ err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+ cl, err := backend.CollectionList(ctx, options)
+ if err != nil {
+ return nil, err
+ }
+ mtx.Lock()
+ defer mtx.Unlock()
+ if len(merged.Items) == 0 {
+ merged = cl
+ } else {
+ merged.Items = append(merged.Items, cl.Items...)
+ }
+ uuids := make([]string, 0, len(cl.Items))
+ for _, item := range cl.Items {
+ uuids = append(uuids, item.UUID)
+ }
+ return uuids, nil
+ })
+ sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+ return merged, err
+}
+
+// Call fn on one or more local/remote backends if opts indicates a
+// federation-wide list query, i.e.:
+//
+// * There is at least one filter of the form
+// ["uuid","in",[a,b,c,...]] or ["uuid","=",a]
+//
+// * One or more of the supplied UUIDs (a,b,c,...) has a non-local
+// prefix.
+//
+// * There are no other filters
+//
+// (If opts doesn't indicate a federation-wide list query, fn is just
+// called once with the local backend.)
+//
+// fn is called more than once only if the query meets the following
+// restrictions:
+//
+// * Count=="none"
+//
+// * Limit<0
+//
+// * len(Order)==0
+//
+// * there are no filters other than the "uuid = ..." and "uuid in
+// ..." filters mentioned above.
+//
+// * The maximum possible response size (total number of objects that
+// could potentially be matched by all of the specified filters)
+// exceeds the local cluster's response page size limit.
+//
+// If the query involves multiple backends but doesn't meet these
+// restrictions, an error is returned without calling fn.
+//
+// Thus, the caller can assume that either:
+//
+// * splitListRequest() returns an error, or
+//
+// * fn is called exactly once, or
+//
+// * fn is called more than once, with options that satisfy the above
+// restrictions.
+//
+// Each call to fn indicates a single (local or remote) backend and a
+// corresponding options argument suitable for sending to that
+// backend.
+func (conn *Conn) splitListRequest(ctx context.Context, opts arvados.ListOptions, fn func(context.Context, string, arvados.API, arvados.ListOptions) ([]string, error)) error {
+ cannotSplit := false
+ var matchAllFilters map[string]bool
+ for _, f := range opts.Filters {
+ matchThisFilter := map[string]bool{}
+ if f.Attr != "uuid" {
+ cannotSplit = true
+ continue
+ }
+ if f.Operator == "=" {
+ if uuid, ok := f.Operand.(string); ok {
+ matchThisFilter[uuid] = true
+ } else {
+ return httpErrorf(http.StatusBadRequest, "invalid operand type %T for filter %q", f.Operand, f)
+ }
+ } else if f.Operator == "in" {
+ if operand, ok := f.Operand.([]interface{}); ok {
+ // skip any elements that aren't
+ // strings (thus can't match a UUID,
+ // thus can't affect the response).
+ for _, v := range operand {
+ if uuid, ok := v.(string); ok {
+ matchThisFilter[uuid] = true
+ }
+ }
+ } else if strings, ok := f.Operand.([]string); ok {
+ for _, uuid := range strings {
+ matchThisFilter[uuid] = true
+ }
+ } else {
+ return httpErrorf(http.StatusBadRequest, "invalid operand type %T in filter %q", f.Operand, f)
+ }
+ } else {
+ cannotSplit = true
+ continue
+ }
+
+ if matchAllFilters == nil {
+ matchAllFilters = matchThisFilter
+ } else {
+ // matchAllFilters = intersect(matchAllFilters, matchThisFilter)
+ for uuid := range matchAllFilters {
+ if !matchThisFilter[uuid] {
+ delete(matchAllFilters, uuid)
+ }
+ }
+ }
+ }
+
+ nUUIDs := 0
+ todoByRemote := map[string]map[string]bool{}
+ for uuid := range matchAllFilters {
+ if len(uuid) != 27 {
+ // Cannot match anything, just drop it
+ } else {
+ if todoByRemote[uuid[:5]] == nil {
+ todoByRemote[uuid[:5]] = map[string]bool{}
+ }
+ todoByRemote[uuid[:5]][uuid] = true
+ nUUIDs++
+ }
+ }
+
+ if len(todoByRemote) > 1 {
+ if cannotSplit {
+ return httpErrorf(http.StatusBadRequest, "cannot execute federated list query with filters other than 'uuid = ...' and 'uuid in [...]'")
+ }
+ if opts.Count != "none" {
+ return httpErrorf(http.StatusBadRequest, "cannot execute federated list query unless count==\"none\"")
+ }
+ if opts.Limit >= 0 || opts.Offset != 0 || len(opts.Order) > 0 {
+ return httpErrorf(http.StatusBadRequest, "cannot execute federated list query with limit, offset, or order parameter")
+ }
+ if max := conn.cluster.API.MaxItemsPerResponse; nUUIDs > max {
+ return httpErrorf(http.StatusBadRequest, "cannot execute federated list query because number of UUIDs (%d) exceeds page size limit %d", nUUIDs, max)
+ }
+ selectingUUID := false
+ for _, attr := range opts.Select {
+ if attr == "uuid" {
+ selectingUUID = true
+ }
+ }
+ if opts.Select != nil && !selectingUUID {
+ return httpErrorf(http.StatusBadRequest, "cannot execute federated list query with a select parameter that does not include uuid")
+ }
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ errs := make(chan error, len(todoByRemote))
+ for clusterID, todo := range todoByRemote {
+ clusterID, todo := clusterID, todo
+ batch := make([]string, 0, len(todo))
+ for uuid := range todo {
+ batch = append(batch, uuid)
+ }
+ go func() {
+ // This goroutine sends exactly one value to
+ // errs.
+ var backend arvados.API
+ if clusterID == conn.cluster.ClusterID {
+ backend = conn.local
+ } else if backend = conn.remotes[clusterID]; backend == nil {
+ errs <- httpErrorf(http.StatusNotFound, "cannot execute federated list query: no proxy available for cluster %q", clusterID)
+ return
+ }
+ remoteOpts := opts
+ for len(todo) > 0 {
+ if len(batch) > len(todo) {
+ // Reduce batch to just the todo's
+ batch = batch[:0]
+ for uuid := range todo {
+ batch = append(batch, uuid)
+ }
+ }
+ remoteOpts.Filters = []arvados.Filter{{"uuid", "in", batch}}
+
+ done, err := fn(ctx, clusterID, backend, remoteOpts)
+ if err != nil {
+ errs <- err
+ return
+ }
+ progress := false
+ for _, uuid := range done {
+ if _, ok := todo[uuid]; ok {
+ progress = true
+ delete(todo, uuid)
+ }
+ }
+ if !progress {
+ errs <- httpErrorf(http.StatusBadGateway, "cannot make progress in federated list query: cluster %q returned none of the requested UUIDs", clusterID)
+ return
+ }
+ }
+ errs <- nil
+ }()
+ }
+
+ // Wait for all goroutines to return, then return the first
+ // non-nil error, if any.
+ var firstErr error
+ for i := 0; i < len(todoByRemote); i++ {
+ if err := <-errs; err != nil && firstErr == nil {
+ firstErr = err
+ // Signal to any remaining fn() calls that
+ // further effort is futile.
+ cancel()
+ }
+ }
+ return firstErr
+}
+
+func httpErrorf(code int, format string, args ...interface{}) error {
+ return httpserver.ErrorWithStatus(fmt.Errorf(format, args...), code)
+}
diff --git a/lib/controller/federation/list_test.go b/lib/controller/federation/list_test.go
new file mode 100644
index 000000000..b28609c2d
--- /dev/null
+++ b/lib/controller/federation/list_test.go
@@ -0,0 +1,438 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+
+ "git.curoverse.com/arvados.git/lib/controller/router"
+ "git.curoverse.com/arvados.git/lib/controller/rpc"
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
+ "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+ "git.curoverse.com/arvados.git/sdk/go/httpserver"
+ check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+ check.TestingT(t)
+}
+
+var (
+ _ = check.Suite(&FederationSuite{})
+ _ = check.Suite(&CollectionListSuite{})
+)
+
+type FederationSuite struct {
+ cluster *arvados.Cluster
+ ctx context.Context
+ fed *Conn
+}
+
+func (s *FederationSuite) SetUpTest(c *check.C) {
+ s.cluster = &arvados.Cluster{
+ ClusterID: "aaaaa",
+ RemoteClusters: map[string]arvados.RemoteCluster{
+ "aaaaa": arvados.RemoteCluster{
+ Host: os.Getenv("ARVADOS_API_HOST"),
+ },
+ },
+ }
+ arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+ s.cluster.TLS.Insecure = true
+ s.cluster.API.MaxItemsPerResponse = 3
+
+ ctx := context.Background()
+ ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
+ ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+ s.ctx = ctx
+
+ s.fed = New(s.cluster)
+}
+
+func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend arvados.API) {
+ s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
+ Host: "in-process.local",
+ }
+ s.fed.remotes[id] = backend
+}
+
+func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend arvados.API) {
+ srv := httpserver.Server{Addr: ":"}
+ srv.Handler = router.New(backend)
+ c.Check(srv.Start(), check.IsNil)
+ s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
+ Host: srv.Addr,
+ Proxy: true,
+ }
+ s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
+}
+
+type collectionLister struct {
+ arvadostest.APIStub
+ ItemsToReturn []arvados.Collection
+ MaxPageSize int
+}
+
+func (cl *collectionLister) matchFilters(c arvados.Collection, filters []arvados.Filter) bool {
+nextfilter:
+ for _, f := range filters {
+ if f.Attr == "uuid" && f.Operator == "=" {
+ s, ok := f.Operand.(string)
+ if ok && s == c.UUID {
+ continue nextfilter
+ }
+ } else if f.Attr == "uuid" && f.Operator == "in" {
+ if operand, ok := f.Operand.([]string); ok {
+ for _, s := range operand {
+ if s == c.UUID {
+ continue nextfilter
+ }
+ }
+ } else if operand, ok := f.Operand.([]interface{}); ok {
+ for _, s := range operand {
+ if s, ok := s.(string); ok && s == c.UUID {
+ continue nextfilter
+ }
+ }
+ }
+ }
+ return false
+ }
+ return true
+}
+
+func (cl *collectionLister) CollectionList(ctx context.Context, options arvados.ListOptions) (resp arvados.CollectionList, _ error) {
+ cl.APIStub.CollectionList(ctx, options)
+ for _, c := range cl.ItemsToReturn {
+ if cl.MaxPageSize > 0 && len(resp.Items) >= cl.MaxPageSize {
+ break
+ }
+ if cl.matchFilters(c, options.Filters) {
+ resp.Items = append(resp.Items, c)
+ }
+ }
+ return
+}
+
+type CollectionListSuite struct {
+ FederationSuite
+ ids []string // aaaaa, bbbbb, ccccc
+ uuids [][]string // [[aa-*, aa-*, aa-*], [bb-*, bb-*, ...], ...]
+ backends []*collectionLister
+}
+
+func (s *CollectionListSuite) SetUpTest(c *check.C) {
+ s.FederationSuite.SetUpTest(c)
+
+ s.ids = nil
+ s.uuids = nil
+ s.backends = nil
+ for i, id := range []string{"aaaaa", "bbbbb", "ccccc"} {
+ cl := &collectionLister{}
+ s.ids = append(s.ids, id)
+ s.uuids = append(s.uuids, nil)
+ for j := 0; j < 5; j++ {
+ uuid := fmt.Sprintf("%s-4zz18-%s%010d", id, id, j)
+ s.uuids[i] = append(s.uuids[i], uuid)
+ cl.ItemsToReturn = append(cl.ItemsToReturn, arvados.Collection{
+ UUID: uuid,
+ })
+ }
+ s.backends = append(s.backends, cl)
+ if i == 0 {
+ s.fed.local = cl
+ } else if i%1 == 0 {
+ // call some backends directly via API
+ s.addDirectRemote(c, id, cl)
+ } else {
+ // call some backends through rpc->router->API
+ // to ensure nothing is lost in translation
+ s.addHTTPRemote(c, id, cl)
+ }
+ }
+}
+
+type listTrial struct {
+ count string
+ limit int
+ offset int
+ order []string
+ filters []arvados.Filter
+ expectUUIDs []string
+ expectCalls []int // number of API calls to backends
+ expectStatus int
+}
+
+func (s *CollectionListSuite) TestCollectionListOneLocal(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "=", s.uuids[0][0]}},
+ expectUUIDs: []string{s.uuids[0][0]},
+ expectCalls: []int{1, 0, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListOneRemote(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "=", s.uuids[1][0]}},
+ expectUUIDs: []string{s.uuids[1][0]},
+ expectCalls: []int{0, 1, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListOneLocalUsingInOperator(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0]}}},
+ expectUUIDs: []string{s.uuids[0][0]},
+ expectCalls: []int{1, 0, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListOneRemoteUsingInOperator(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[1][1]}}},
+ expectUUIDs: []string{s.uuids[1][1]},
+ expectCalls: []int{0, 1, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListOneLocalOneRemote(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}}},
+ expectUUIDs: []string{s.uuids[0][0], s.uuids[1][0]},
+ expectCalls: []int{1, 1, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListTwoRemotes(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[2][0], s.uuids[1][0]}}},
+ expectUUIDs: []string{s.uuids[1][0], s.uuids[2][0]},
+ expectCalls: []int{0, 1, 1},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListSatisfyAllFilters(c *check.C) {
+ s.cluster.API.MaxItemsPerResponse = 2
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][1], s.uuids[2][0], s.uuids[2][1], s.uuids[2][2]}},
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][2], s.uuids[2][1]}},
+ },
+ expectUUIDs: []string{s.uuids[0][0], s.uuids[2][1]},
+ expectCalls: []int{1, 0, 1},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListEmptySet(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", []string{}}},
+ expectUUIDs: []string{},
+ expectCalls: []int{0, 0, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListUnmatchableUUID(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], "abcdefg"}},
+ {"uuid", "in", []string{s.uuids[0][0], "bbbbb-4zz18-bogus"}},
+ {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-bogus"}},
+ },
+ expectUUIDs: []string{s.uuids[0][0]},
+ expectCalls: []int{1, 0, 0},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiPage(c *check.C) {
+ for i := range s.backends {
+ s.uuids[i] = s.uuids[i][:3]
+ s.backends[i].ItemsToReturn = s.backends[i].ItemsToReturn[:3]
+ }
+ s.cluster.API.MaxItemsPerResponse = 9
+ for _, stub := range s.backends {
+ stub.MaxPageSize = 2
+ }
+ allUUIDs := append(append(append([]string(nil), s.uuids[0]...), s.uuids[1]...), s.uuids[2]...)
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{{"uuid", "in", append([]string(nil), allUUIDs...)}},
+ expectUUIDs: allUUIDs,
+ expectCalls: []int{2, 2, 2},
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiSiteExtraFilters(c *check.C) {
+ // not [yet] supported
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ {"uuid", "is_a", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiSiteWithCount(c *check.C) {
+ for _, count := range []string{"", "exact"} {
+ s.test(c, listTrial{
+ count: count,
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ {"uuid", "is_a", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+ }
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiSiteWithLimit(c *check.C) {
+ for _, limit := range []int{0, 1, 2} {
+ s.test(c, listTrial{
+ count: "none",
+ limit: limit,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ {"uuid", "is_a", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+ }
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiSiteWithOffset(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ offset: 1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ {"uuid", "is_a", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListMultiSiteWithOrder(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ order: []string{"uuid desc"},
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ {"uuid", "is_a", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListInvalidFilters(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", "teapot"},
+ },
+ expectCalls: []int{0, 0, 0},
+ expectStatus: http.StatusBadRequest,
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListRemoteUnknown(c *check.C) {
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-000001111122222"}},
+ },
+ expectStatus: http.StatusNotFound,
+ })
+}
+
+func (s *CollectionListSuite) TestCollectionListRemoteError(c *check.C) {
+ s.addDirectRemote(c, "bbbbb", &arvadostest.APIStub{})
+ s.test(c, listTrial{
+ count: "none",
+ limit: -1,
+ filters: []arvados.Filter{
+ {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
+ },
+ expectStatus: http.StatusBadGateway,
+ })
+}
+
+func (s *CollectionListSuite) test(c *check.C, trial listTrial) {
+ resp, err := s.fed.CollectionList(s.ctx, arvados.ListOptions{
+ Count: trial.count,
+ Limit: trial.limit,
+ Offset: trial.offset,
+ Order: trial.order,
+ Filters: trial.filters,
+ })
+ if trial.expectStatus != 0 {
+ c.Assert(err, check.NotNil)
+ err, _ := err.(interface{ HTTPStatus() int })
+ c.Assert(err, check.NotNil) // err must implement HTTPStatus()
+ c.Check(err.HTTPStatus(), check.Equals, trial.expectStatus)
+ c.Logf("returned error is %#v", err)
+ c.Logf("returned error string is %q", err)
+ } else {
+ c.Check(err, check.IsNil)
+ var expectItems []arvados.Collection
+ for _, uuid := range trial.expectUUIDs {
+ expectItems = append(expectItems, arvados.Collection{UUID: uuid})
+ }
+ c.Check(resp, check.DeepEquals, arvados.CollectionList{
+ Items: expectItems,
+ })
+ }
+
+ for i, stub := range s.backends {
+ if i >= len(trial.expectCalls) {
+ break
+ }
+ calls := stub.Calls(nil)
+ c.Check(calls, check.HasLen, trial.expectCalls[i])
+ if len(calls) == 0 {
+ continue
+ }
+ opts := calls[0].Options.(arvados.ListOptions)
+ c.Check(opts.Limit, check.Equals, -1)
+ }
+}
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index d524195e4..852327fd8 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -18,6 +18,7 @@ import (
"time"
"git.curoverse.com/arvados.git/lib/config"
+ "git.curoverse.com/arvados.git/lib/controller/federation"
"git.curoverse.com/arvados.git/lib/controller/railsproxy"
"git.curoverse.com/arvados.git/lib/controller/router"
"git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -91,7 +92,7 @@ func (h *Handler) setup() {
}))
if h.Cluster.EnableBetaController14287 {
- rtr := router.New(h.Cluster)
+ rtr := router.New(federation.New(h.Cluster))
mux.Handle("/arvados/v1/collections", rtr)
mux.Handle("/arvados/v1/collections/", rtr)
}
diff --git a/lib/controller/router/router.go b/lib/controller/router/router.go
index f37c7ea90..9c2c1f3a1 100644
--- a/lib/controller/router/router.go
+++ b/lib/controller/router/router.go
@@ -10,7 +10,6 @@ import (
"net/http"
"strings"
- "git.curoverse.com/arvados.git/lib/controller/federation"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/ctxlog"
@@ -24,10 +23,10 @@ type router struct {
fed arvados.API
}
-func New(cluster *arvados.Cluster) *router {
+func New(fed arvados.API) *router {
rtr := &router{
mux: httprouter.New(),
- fed: federation.New(cluster),
+ fed: fed,
}
rtr.addRoutes()
return rtr
diff --git a/lib/controller/router/router_test.go b/lib/controller/router/router_test.go
index 4e6b16173..3a7045aa4 100644
--- a/lib/controller/router/router_test.go
+++ b/lib/controller/router/router_test.go
@@ -10,11 +10,13 @@ import (
"io"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"strings"
"testing"
"time"
+ "git.curoverse.com/arvados.git/lib/controller/rpc"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"github.com/julienschmidt/httprouter"
@@ -158,7 +160,8 @@ func (s *RouterIntegrationSuite) SetUpTest(c *check.C) {
cluster := &arvados.Cluster{}
cluster.TLS.Insecure = true
arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
- s.rtr = New(cluster)
+ url, _ := url.Parse("https://" + os.Getenv("ARVADOS_TEST_API_HOST"))
+ s.rtr = New(rpc.NewConn("zzzzz", url, true, rpc.PassthroughTokenProvider))
}
func (s *RouterIntegrationSuite) TearDownSuite(c *check.C) {
diff --git a/lib/controller/rpc/conn.go b/lib/controller/rpc/conn.go
index e07eaf40a..ea3d6fb2d 100644
--- a/lib/controller/rpc/conn.go
+++ b/lib/controller/rpc/conn.go
@@ -8,6 +8,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
+ "errors"
"fmt"
"io"
"net"
@@ -17,10 +18,19 @@ import (
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
)
type TokenProvider func(context.Context) ([]string, error)
+func PassthroughTokenProvider(ctx context.Context) ([]string, error) {
+ if incoming, ok := auth.FromContext(ctx); !ok {
+ return nil, errors.New("no token provided")
+ } else {
+ return incoming.Tokens, nil
+ }
+}
+
type Conn struct {
clusterID string
httpClient http.Client
diff --git a/sdk/go/arvadostest/api.go b/sdk/go/arvadostest/api.go
index a3cacf3f6..77a26bcba 100644
--- a/sdk/go/arvadostest/api.go
+++ b/sdk/go/arvadostest/api.go
@@ -7,6 +7,8 @@ package arvadostest
import (
"context"
"errors"
+ "reflect"
+ "runtime"
"sync"
"git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -121,7 +123,9 @@ func (as *APIStub) Calls(method interface{}) []APIStubCall {
defer as.mtx.Unlock()
var calls []APIStubCall
for _, call := range as.calls {
- if method == nil || call.Method == method {
+
+ if method == nil || (runtime.FuncForPC(reflect.ValueOf(call.Method).Pointer()).Name() ==
+ runtime.FuncForPC(reflect.ValueOf(method).Pointer()).Name()) {
calls = append(calls, call)
}
}
diff --git a/sdk/go/httpserver/error.go b/sdk/go/httpserver/error.go
index b222e18ea..f1817d337 100644
--- a/sdk/go/httpserver/error.go
+++ b/sdk/go/httpserver/error.go
@@ -9,6 +9,19 @@ import (
"net/http"
)
+func ErrorWithStatus(err error, status int) error {
+ return errorWithStatus{err, status}
+}
+
+type errorWithStatus struct {
+ error
+ Status int
+}
+
+func (ews errorWithStatus) HTTPStatus() int {
+ return ews.Status
+}
+
type ErrorResponse struct {
Errors []string `json:"errors"`
}
commit 45f4606784d093a5d7e63ce78b432e4e67ccb04b
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date: Tue Jul 2 10:10:41 2019 -0400
14287: Shut down RailsAPI before trying to reinstall.
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>
diff --git a/build/run-tests.sh b/build/run-tests.sh
index 94ef9ef3d..c682024e3 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -923,6 +923,7 @@ install_services/login-sync() {
}
install_services/api() {
+ stop_services
cd "$WORKSPACE/services/api" \
&& RAILS_ENV=test bundle_install_trylocal
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list