Merge branch '15851-bundle'
[arvados.git] / lib / controller / federation / list_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package federation
6
7 import (
8         "context"
9         "fmt"
10         "net/http"
11         "net/url"
12         "os"
13         "sort"
14         "testing"
15
16         "git.curoverse.com/arvados.git/lib/controller/router"
17         "git.curoverse.com/arvados.git/lib/controller/rpc"
18         "git.curoverse.com/arvados.git/sdk/go/arvados"
19         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
20         "git.curoverse.com/arvados.git/sdk/go/auth"
21         "git.curoverse.com/arvados.git/sdk/go/ctxlog"
22         "git.curoverse.com/arvados.git/sdk/go/httpserver"
23         check "gopkg.in/check.v1"
24 )
25
26 // Gocheck boilerplate
27 func Test(t *testing.T) {
28         check.TestingT(t)
29 }
30
31 var (
32         _ = check.Suite(&FederationSuite{})
33         _ = check.Suite(&CollectionListSuite{})
34 )
35
36 type FederationSuite struct {
37         cluster *arvados.Cluster
38         ctx     context.Context
39         fed     *Conn
40 }
41
42 func (s *FederationSuite) SetUpTest(c *check.C) {
43         s.cluster = &arvados.Cluster{
44                 ClusterID: "aaaaa",
45                 RemoteClusters: map[string]arvados.RemoteCluster{
46                         "aaaaa": arvados.RemoteCluster{
47                                 Host: os.Getenv("ARVADOS_API_HOST"),
48                         },
49                 },
50         }
51         arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
52         s.cluster.TLS.Insecure = true
53         s.cluster.API.MaxItemsPerResponse = 3
54
55         ctx := context.Background()
56         ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
57         ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
58         s.ctx = ctx
59
60         s.fed = New(s.cluster)
61 }
62
63 func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
64         s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
65                 Host: "in-process.local",
66         }
67         s.fed.remotes[id] = backend
68 }
69
70 func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
71         srv := httpserver.Server{Addr: ":"}
72         srv.Handler = router.New(backend)
73         c.Check(srv.Start(), check.IsNil)
74         s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
75                 Host:  srv.Addr,
76                 Proxy: true,
77         }
78         s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
79 }
80
81 type collectionLister struct {
82         arvadostest.APIStub
83         ItemsToReturn []arvados.Collection
84         MaxPageSize   int
85 }
86
87 func (cl *collectionLister) matchFilters(c arvados.Collection, filters []arvados.Filter) bool {
88 nextfilter:
89         for _, f := range filters {
90                 if f.Attr == "uuid" && f.Operator == "=" {
91                         s, ok := f.Operand.(string)
92                         if ok && s == c.UUID {
93                                 continue nextfilter
94                         }
95                 } else if f.Attr == "uuid" && f.Operator == "in" {
96                         if operand, ok := f.Operand.([]string); ok {
97                                 for _, s := range operand {
98                                         if s == c.UUID {
99                                                 continue nextfilter
100                                         }
101                                 }
102                         } else if operand, ok := f.Operand.([]interface{}); ok {
103                                 for _, s := range operand {
104                                         if s, ok := s.(string); ok && s == c.UUID {
105                                                 continue nextfilter
106                                         }
107                                 }
108                         }
109                 }
110                 return false
111         }
112         return true
113 }
114
115 func (cl *collectionLister) CollectionList(ctx context.Context, options arvados.ListOptions) (resp arvados.CollectionList, _ error) {
116         cl.APIStub.CollectionList(ctx, options)
117         for _, c := range cl.ItemsToReturn {
118                 if cl.MaxPageSize > 0 && len(resp.Items) >= cl.MaxPageSize {
119                         break
120                 }
121                 if options.Limit >= 0 && len(resp.Items) >= options.Limit {
122                         break
123                 }
124                 if cl.matchFilters(c, options.Filters) {
125                         resp.Items = append(resp.Items, c)
126                 }
127         }
128         return
129 }
130
131 type CollectionListSuite struct {
132         FederationSuite
133         ids      []string   // aaaaa, bbbbb, ccccc
134         uuids    [][]string // [[aa-*, aa-*, aa-*], [bb-*, bb-*, ...], ...]
135         backends []*collectionLister
136 }
137
138 func (s *CollectionListSuite) SetUpTest(c *check.C) {
139         s.FederationSuite.SetUpTest(c)
140
141         s.ids = nil
142         s.uuids = nil
143         s.backends = nil
144         for i, id := range []string{"aaaaa", "bbbbb", "ccccc"} {
145                 cl := &collectionLister{}
146                 s.ids = append(s.ids, id)
147                 s.uuids = append(s.uuids, nil)
148                 for j := 0; j < 5; j++ {
149                         uuid := fmt.Sprintf("%s-4zz18-%s%010d", id, id, j)
150                         s.uuids[i] = append(s.uuids[i], uuid)
151                         cl.ItemsToReturn = append(cl.ItemsToReturn, arvados.Collection{
152                                 UUID: uuid,
153                         })
154                 }
155                 s.backends = append(s.backends, cl)
156                 if i == 0 {
157                         s.fed.local = cl
158                 } else if i%1 == 0 {
159                         // call some backends directly via API
160                         s.addDirectRemote(c, id, cl)
161                 } else {
162                         // call some backends through rpc->router->API
163                         // to ensure nothing is lost in translation
164                         s.addHTTPRemote(c, id, cl)
165                 }
166         }
167 }
168
169 type listTrial struct {
170         count        string
171         limit        int
172         offset       int
173         order        []string
174         filters      []arvados.Filter
175         expectUUIDs  []string
176         expectCalls  []int // number of API calls to backends
177         expectStatus int
178 }
179
180 func (s *CollectionListSuite) TestCollectionListNoUUIDFilters(c *check.C) {
181         s.test(c, listTrial{
182                 count:       "none",
183                 limit:       1,
184                 expectUUIDs: []string{s.uuids[0][0]},
185                 expectCalls: []int{1, 0, 0},
186         })
187 }
188
189 func (s *CollectionListSuite) TestCollectionListOneLocal(c *check.C) {
190         s.test(c, listTrial{
191                 count:       "none",
192                 limit:       -1,
193                 filters:     []arvados.Filter{{"uuid", "=", s.uuids[0][0]}},
194                 expectUUIDs: []string{s.uuids[0][0]},
195                 expectCalls: []int{1, 0, 0},
196         })
197 }
198
199 func (s *CollectionListSuite) TestCollectionListOneRemote(c *check.C) {
200         s.test(c, listTrial{
201                 count:       "none",
202                 limit:       -1,
203                 filters:     []arvados.Filter{{"uuid", "=", s.uuids[1][0]}},
204                 expectUUIDs: []string{s.uuids[1][0]},
205                 expectCalls: []int{0, 1, 0},
206         })
207 }
208
209 func (s *CollectionListSuite) TestCollectionListOneLocalUsingInOperator(c *check.C) {
210         s.test(c, listTrial{
211                 count:       "none",
212                 limit:       -1,
213                 filters:     []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0]}}},
214                 expectUUIDs: []string{s.uuids[0][0]},
215                 expectCalls: []int{1, 0, 0},
216         })
217 }
218
219 func (s *CollectionListSuite) TestCollectionListOneRemoteUsingInOperator(c *check.C) {
220         s.test(c, listTrial{
221                 count:       "none",
222                 limit:       -1,
223                 filters:     []arvados.Filter{{"uuid", "in", []string{s.uuids[1][1]}}},
224                 expectUUIDs: []string{s.uuids[1][1]},
225                 expectCalls: []int{0, 1, 0},
226         })
227 }
228
229 func (s *CollectionListSuite) TestCollectionListOneLocalOneRemote(c *check.C) {
230         s.test(c, listTrial{
231                 count:       "none",
232                 limit:       -1,
233                 filters:     []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}}},
234                 expectUUIDs: []string{s.uuids[0][0], s.uuids[1][0]},
235                 expectCalls: []int{1, 1, 0},
236         })
237 }
238
239 func (s *CollectionListSuite) TestCollectionListTwoRemotes(c *check.C) {
240         s.test(c, listTrial{
241                 count:       "none",
242                 limit:       -1,
243                 filters:     []arvados.Filter{{"uuid", "in", []string{s.uuids[2][0], s.uuids[1][0]}}},
244                 expectUUIDs: []string{s.uuids[1][0], s.uuids[2][0]},
245                 expectCalls: []int{0, 1, 1},
246         })
247 }
248
249 func (s *CollectionListSuite) TestCollectionListSatisfyAllFilters(c *check.C) {
250         s.cluster.API.MaxItemsPerResponse = 2
251         s.test(c, listTrial{
252                 count: "none",
253                 limit: -1,
254                 filters: []arvados.Filter{
255                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][1], s.uuids[2][0], s.uuids[2][1], s.uuids[2][2]}},
256                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][2], s.uuids[2][1]}},
257                 },
258                 expectUUIDs: []string{s.uuids[0][0], s.uuids[2][1]},
259                 expectCalls: []int{1, 0, 1},
260         })
261 }
262
263 func (s *CollectionListSuite) TestCollectionListEmptySet(c *check.C) {
264         s.test(c, listTrial{
265                 count:       "none",
266                 limit:       -1,
267                 filters:     []arvados.Filter{{"uuid", "in", []string{}}},
268                 expectUUIDs: []string{},
269                 expectCalls: []int{0, 0, 0},
270         })
271 }
272
273 func (s *CollectionListSuite) TestCollectionListUnmatchableUUID(c *check.C) {
274         s.test(c, listTrial{
275                 count: "none",
276                 limit: -1,
277                 filters: []arvados.Filter{
278                         {"uuid", "in", []string{s.uuids[0][0], "abcdefg"}},
279                         {"uuid", "in", []string{s.uuids[0][0], "bbbbb-4zz18-bogus"}},
280                         {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-bogus"}},
281                 },
282                 expectUUIDs: []string{s.uuids[0][0]},
283                 expectCalls: []int{1, 0, 0},
284         })
285 }
286
287 func (s *CollectionListSuite) TestCollectionListMultiPage(c *check.C) {
288         for i := range s.backends {
289                 s.uuids[i] = s.uuids[i][:3]
290                 s.backends[i].ItemsToReturn = s.backends[i].ItemsToReturn[:3]
291         }
292         s.cluster.API.MaxItemsPerResponse = 9
293         for _, stub := range s.backends {
294                 stub.MaxPageSize = 2
295         }
296         allUUIDs := append(append(append([]string(nil), s.uuids[0]...), s.uuids[1]...), s.uuids[2]...)
297         s.test(c, listTrial{
298                 count:       "none",
299                 limit:       -1,
300                 filters:     []arvados.Filter{{"uuid", "in", append([]string(nil), allUUIDs...)}},
301                 expectUUIDs: allUUIDs,
302                 expectCalls: []int{2, 2, 2},
303         })
304 }
305
306 func (s *CollectionListSuite) TestCollectionListMultiSiteExtraFilters(c *check.C) {
307         // not [yet] supported
308         s.test(c, listTrial{
309                 count: "none",
310                 limit: -1,
311                 filters: []arvados.Filter{
312                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
313                         {"uuid", "is_a", "teapot"},
314                 },
315                 expectCalls:  []int{0, 0, 0},
316                 expectStatus: http.StatusBadRequest,
317         })
318 }
319
320 func (s *CollectionListSuite) TestCollectionListMultiSiteWithCount(c *check.C) {
321         for _, count := range []string{"", "exact"} {
322                 s.test(c, listTrial{
323                         count: count,
324                         limit: -1,
325                         filters: []arvados.Filter{
326                                 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
327                                 {"uuid", "is_a", "teapot"},
328                         },
329                         expectCalls:  []int{0, 0, 0},
330                         expectStatus: http.StatusBadRequest,
331                 })
332         }
333 }
334
335 func (s *CollectionListSuite) TestCollectionListMultiSiteWithLimit(c *check.C) {
336         for _, limit := range []int{0, 1, 2} {
337                 s.test(c, listTrial{
338                         count: "none",
339                         limit: limit,
340                         filters: []arvados.Filter{
341                                 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
342                                 {"uuid", "is_a", "teapot"},
343                         },
344                         expectCalls:  []int{0, 0, 0},
345                         expectStatus: http.StatusBadRequest,
346                 })
347         }
348 }
349
350 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOffset(c *check.C) {
351         s.test(c, listTrial{
352                 count:  "none",
353                 limit:  -1,
354                 offset: 1,
355                 filters: []arvados.Filter{
356                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
357                         {"uuid", "is_a", "teapot"},
358                 },
359                 expectCalls:  []int{0, 0, 0},
360                 expectStatus: http.StatusBadRequest,
361         })
362 }
363
364 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOrder(c *check.C) {
365         s.test(c, listTrial{
366                 count: "none",
367                 limit: -1,
368                 order: []string{"uuid desc"},
369                 filters: []arvados.Filter{
370                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
371                         {"uuid", "is_a", "teapot"},
372                 },
373                 expectCalls:  []int{0, 0, 0},
374                 expectStatus: http.StatusBadRequest,
375         })
376 }
377
378 func (s *CollectionListSuite) TestCollectionListInvalidFilters(c *check.C) {
379         s.test(c, listTrial{
380                 count: "none",
381                 limit: -1,
382                 filters: []arvados.Filter{
383                         {"uuid", "in", "teapot"},
384                 },
385                 expectCalls:  []int{0, 0, 0},
386                 expectStatus: http.StatusBadRequest,
387         })
388 }
389
390 func (s *CollectionListSuite) TestCollectionListRemoteUnknown(c *check.C) {
391         s.test(c, listTrial{
392                 count: "none",
393                 limit: -1,
394                 filters: []arvados.Filter{
395                         {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-000001111122222"}},
396                 },
397                 expectStatus: http.StatusNotFound,
398         })
399 }
400
401 func (s *CollectionListSuite) TestCollectionListRemoteError(c *check.C) {
402         s.addDirectRemote(c, "bbbbb", &arvadostest.APIStub{})
403         s.test(c, listTrial{
404                 count: "none",
405                 limit: -1,
406                 filters: []arvados.Filter{
407                         {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
408                 },
409                 expectStatus: http.StatusBadGateway,
410         })
411 }
412
413 func (s *CollectionListSuite) test(c *check.C, trial listTrial) {
414         resp, err := s.fed.CollectionList(s.ctx, arvados.ListOptions{
415                 Count:   trial.count,
416                 Limit:   trial.limit,
417                 Offset:  trial.offset,
418                 Order:   trial.order,
419                 Filters: trial.filters,
420         })
421         if trial.expectStatus != 0 {
422                 c.Assert(err, check.NotNil)
423                 err, _ := err.(interface{ HTTPStatus() int })
424                 c.Assert(err, check.NotNil) // err must implement HTTPStatus()
425                 c.Check(err.HTTPStatus(), check.Equals, trial.expectStatus)
426                 c.Logf("returned error is %#v", err)
427                 c.Logf("returned error string is %q", err)
428         } else {
429                 c.Check(err, check.IsNil)
430                 expectItems := []arvados.Collection{}
431                 for _, uuid := range trial.expectUUIDs {
432                         expectItems = append(expectItems, arvados.Collection{UUID: uuid})
433                 }
434                 // expectItems is sorted by UUID, so sort resp.Items
435                 // by UUID before checking DeepEquals.
436                 sort.Slice(resp.Items, func(i, j int) bool { return resp.Items[i].UUID < resp.Items[j].UUID })
437                 c.Check(resp, check.DeepEquals, arvados.CollectionList{
438                         Items: expectItems,
439                 })
440         }
441
442         for i, stub := range s.backends {
443                 if i >= len(trial.expectCalls) {
444                         break
445                 }
446                 calls := stub.Calls(nil)
447                 c.Check(calls, check.HasLen, trial.expectCalls[i])
448                 if len(calls) == 0 {
449                         continue
450                 }
451                 opts := calls[0].Options.(arvados.ListOptions)
452                 c.Check(opts.Limit, check.Equals, trial.limit)
453         }
454 }