1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
17 "git.arvados.org/arvados.git/sdk/go/arvados"
18 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
19 "git.arvados.org/arvados.git/sdk/go/arvadostest"
20 "git.arvados.org/arvados.git/sdk/go/auth"
21 "git.arvados.org/arvados.git/sdk/go/keepclient"
22 check "gopkg.in/check.v1"
25 var _ = check.Suite(&CollectionSuite{})
27 type CollectionSuite struct {
31 func (s *CollectionSuite) TestCollectionCreateAndUpdateWithProperties(c *check.C) {
32 s.setUpVocabulary(c, "")
33 ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
37 props map[string]interface{}
40 {"Invalid prop key", map[string]interface{}{"Priority": "IDVALIMPORTANCES1"}, false},
41 {"Invalid prop value", map[string]interface{}{"IDTAGIMPORTANCES": "high"}, false},
42 {"Valid prop key & value", map[string]interface{}{"IDTAGIMPORTANCES": "IDVALIMPORTANCES1"}, true},
43 {"Empty properties", map[string]interface{}{}, true},
45 for _, tt := range tests {
46 c.Log(c.TestName()+" ", tt.name)
48 // Create with properties
49 coll, err := s.localdb.CollectionCreate(ctx, arvados.CreateOptions{
50 Select: []string{"uuid", "properties"},
51 Attrs: map[string]interface{}{
52 "properties": tt.props,
55 c.Assert(err, check.IsNil)
56 c.Assert(coll.Properties, check.DeepEquals, tt.props)
58 c.Assert(err, check.NotNil)
61 // Create, then update with properties
62 coll, err = s.localdb.CollectionCreate(ctx, arvados.CreateOptions{})
63 c.Assert(err, check.IsNil)
64 coll, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
66 Select: []string{"uuid", "properties"},
67 Attrs: map[string]interface{}{
68 "properties": tt.props,
71 c.Assert(err, check.IsNil)
72 c.Assert(coll.Properties, check.DeepEquals, tt.props)
74 c.Assert(err, check.NotNil)
79 func (s *CollectionSuite) TestCollectionReplaceFiles(c *check.C) {
80 ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.AdminToken}})
81 foo, err := s.localdb.railsProxy.CollectionCreate(ctx, arvados.CreateOptions{
82 Attrs: map[string]interface{}{
83 "owner_uuid": arvadostest.ActiveUserUUID,
84 "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt\n",
86 c.Assert(err, check.IsNil)
87 s.localdb.signCollection(ctx, &foo)
88 foobarbaz, err := s.localdb.railsProxy.CollectionCreate(ctx, arvados.CreateOptions{
89 Attrs: map[string]interface{}{
90 "owner_uuid": arvadostest.ActiveUserUUID,
91 "manifest_text": "./foo/bar 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz.txt\n",
93 c.Assert(err, check.IsNil)
94 s.localdb.signCollection(ctx, &foobarbaz)
95 wazqux, err := s.localdb.railsProxy.CollectionCreate(ctx, arvados.CreateOptions{
96 Attrs: map[string]interface{}{
97 "owner_uuid": arvadostest.ActiveUserUUID,
98 "manifest_text": "./waz d85b1213473c2fd7c2045020a6b9c62b+3 0:3:qux.txt\n",
100 c.Assert(err, check.IsNil)
101 s.localdb.signCollection(ctx, &wazqux)
103 ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
105 // Create using content from existing collections
106 dst, err := s.localdb.CollectionCreate(ctx, arvados.CreateOptions{
107 ReplaceFiles: map[string]string{
108 "/f": foo.PortableDataHash + "/foo.txt",
109 "/b": foobarbaz.PortableDataHash + "/foo/bar",
110 "/q": wazqux.PortableDataHash + "/",
111 "/w": wazqux.PortableDataHash + "/waz",
113 Attrs: map[string]interface{}{
114 "owner_uuid": arvadostest.ActiveUserUUID,
116 c.Assert(err, check.IsNil)
117 s.expectFiles(c, dst, "f", "b/baz.txt", "q/waz/qux.txt", "w/qux.txt")
119 // Delete a file and a directory
120 dst, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
122 ReplaceFiles: map[string]string{
126 c.Assert(err, check.IsNil)
127 s.expectFiles(c, dst, "b/baz.txt", "q/", "w/qux.txt")
129 // Move and copy content within collection
130 dst, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
132 ReplaceFiles: map[string]string{
133 // Note splicing content to /b/corge.txt but
134 // removing everything else from /b
136 "/b/corge.txt": dst.PortableDataHash + "/b/baz.txt",
137 "/quux/corge.txt": dst.PortableDataHash + "/b/baz.txt",
139 c.Assert(err, check.IsNil)
140 s.expectFiles(c, dst, "b/corge.txt", "q/", "w/qux.txt", "quux/corge.txt")
142 // Remove everything except one file
143 dst, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
145 ReplaceFiles: map[string]string{
147 "/b/corge.txt": dst.PortableDataHash + "/b/corge.txt",
149 c.Assert(err, check.IsNil)
150 s.expectFiles(c, dst, "b/corge.txt")
152 // Copy entire collection to root
153 dstcopy, err := s.localdb.CollectionCreate(ctx, arvados.CreateOptions{
154 ReplaceFiles: map[string]string{
155 "/": dst.PortableDataHash,
157 c.Check(err, check.IsNil)
158 c.Check(dstcopy.PortableDataHash, check.Equals, dst.PortableDataHash)
159 s.expectFiles(c, dstcopy, "b/corge.txt")
161 // Check invalid targets, sources, and combinations
162 for _, badrepl := range []map[string]string{
164 "/foo/nope": dst.PortableDataHash + "/b",
165 "/foo": dst.PortableDataHash + "/b",
168 "/foo": dst.PortableDataHash + "/b",
172 "/": dst.PortableDataHash + "/",
176 "/": dst.PortableDataHash + "/",
177 "/nope": dst.PortableDataHash + "/b",
189 {"/bad": dst.UUID + "/b"},
191 _, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
193 ReplaceFiles: badrepl,
195 c.Logf("badrepl %#v\n... got err: %s", badrepl, err)
196 c.Check(err, check.NotNil)
199 // Check conflicting replace_files and manifest_text
200 _, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
202 ReplaceFiles: map[string]string{"/": ""},
203 Attrs: map[string]interface{}{
204 "manifest_text": ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:z\n",
206 c.Logf("replace_files+manifest_text\n... got err: %s", err)
207 c.Check(err, check.ErrorMatches, "ambiguous request: both.*replace_files.*manifest_text.*")
210 // expectFiles checks coll's directory structure against the given
211 // list of expected files and empty directories. An expected path with
212 // a trailing slash indicates an empty directory.
213 func (s *CollectionSuite) expectFiles(c *check.C, coll arvados.Collection, expected ...string) {
214 client := arvados.NewClientFromEnv()
215 ac, err := arvadosclient.New(client)
216 c.Assert(err, check.IsNil)
217 kc, err := keepclient.MakeKeepClient(ac)
218 c.Assert(err, check.IsNil)
219 cfs, err := coll.FileSystem(arvados.NewClientFromEnv(), kc)
220 c.Assert(err, check.IsNil)
222 nonemptydirs := map[string]bool{}
223 fs.WalkDir(arvados.FS(cfs), "/", func(path string, d fs.DirEntry, err error) error {
224 dir, _ := filepath.Split(path)
225 nonemptydirs[dir] = true
230 if !nonemptydirs[path] {
231 nonemptydirs[path] = false
234 found = append(found, path)
238 for d, nonempty := range nonemptydirs {
240 found = append(found, d)
243 for i, path := range found {
245 found[i] = strings.TrimPrefix(path, "/")
249 sort.Strings(expected)
250 c.Check(found, check.DeepEquals, expected)
253 func (s *CollectionSuite) TestSignatures(c *check.C) {
254 ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
256 resp, err := s.localdb.CollectionGet(ctx, arvados.GetOptions{UUID: arvadostest.FooCollection})
257 c.Check(err, check.IsNil)
258 c.Check(resp.ManifestText, check.Matches, `(?ms).* acbd[^ ]*\+3\+A[0-9a-f]+@[0-9a-f]+ 0:.*`)
259 s.checkSignatureExpiry(c, resp.ManifestText, time.Hour*24*7*2)
261 resp, err = s.localdb.CollectionGet(ctx, arvados.GetOptions{UUID: arvadostest.FooCollection, Select: []string{"manifest_text"}})
262 c.Check(err, check.IsNil)
263 c.Check(resp.ManifestText, check.Matches, `(?ms).* acbd[^ ]*\+3\+A[0-9a-f]+@[0-9a-f]+ 0:.*`)
265 lresp, err := s.localdb.CollectionList(ctx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", arvadostest.FooCollection}}})
266 c.Check(err, check.IsNil)
267 if c.Check(lresp.Items, check.HasLen, 1) {
268 c.Check(lresp.Items[0].UUID, check.Equals, arvadostest.FooCollection)
269 c.Check(lresp.Items[0].ManifestText, check.Equals, "")
270 c.Check(lresp.Items[0].UnsignedManifestText, check.Equals, "")
273 lresp, err = s.localdb.CollectionList(ctx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", arvadostest.FooCollection}}, Select: []string{"manifest_text"}})
274 c.Check(err, check.IsNil)
275 if c.Check(lresp.Items, check.HasLen, 1) {
276 c.Check(lresp.Items[0].ManifestText, check.Matches, `(?ms).* acbd[^ ]*\+3\+A[0-9a-f]+@[0-9a-f]+ 0:.*`)
277 c.Check(lresp.Items[0].UnsignedManifestText, check.Equals, "")
280 lresp, err = s.localdb.CollectionList(ctx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", arvadostest.FooCollection}}, Select: []string{"unsigned_manifest_text"}})
281 c.Check(err, check.IsNil)
282 if c.Check(lresp.Items, check.HasLen, 1) {
283 c.Check(lresp.Items[0].ManifestText, check.Equals, "")
284 c.Check(lresp.Items[0].UnsignedManifestText, check.Matches, `(?ms).* acbd[^ ]*\+3 0:.*`)
287 // early trash date causes lower signature TTL (even if
288 // trash_at and is_trashed fields are unselected)
289 trashed, err := s.localdb.CollectionCreate(ctx, arvados.CreateOptions{
290 Select: []string{"uuid", "manifest_text"},
291 Attrs: map[string]interface{}{
292 "manifest_text": ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
293 "trash_at": time.Now().UTC().Add(time.Hour),
295 c.Assert(err, check.IsNil)
296 s.checkSignatureExpiry(c, trashed.ManifestText, time.Hour)
297 resp, err = s.localdb.CollectionGet(ctx, arvados.GetOptions{UUID: trashed.UUID})
298 c.Assert(err, check.IsNil)
299 s.checkSignatureExpiry(c, resp.ManifestText, time.Hour)
301 // distant future trash date does not cause higher signature TTL
302 trashed, err = s.localdb.CollectionUpdate(ctx, arvados.UpdateOptions{
304 Attrs: map[string]interface{}{
305 "trash_at": time.Now().UTC().Add(time.Hour * 24 * 365),
307 c.Assert(err, check.IsNil)
308 s.checkSignatureExpiry(c, trashed.ManifestText, time.Hour*24*7*2)
309 resp, err = s.localdb.CollectionGet(ctx, arvados.GetOptions{UUID: trashed.UUID})
310 c.Assert(err, check.IsNil)
311 s.checkSignatureExpiry(c, resp.ManifestText, time.Hour*24*7*2)
313 // Make sure groups/contents doesn't return manifest_text with
314 // collections (if it did, we'd need to sign it).
315 gresp, err := s.localdb.GroupContents(ctx, arvados.GroupContentsOptions{
317 Filters: []arvados.Filter{{"uuid", "=", arvadostest.FooCollection}},
318 Select: []string{"uuid", "manifest_text"},
321 c.Check(err, check.ErrorMatches, `.*Invalid attribute.*manifest_text.*`)
322 } else if c.Check(gresp.Items, check.HasLen, 1) {
323 c.Check(gresp.Items[0].(map[string]interface{})["uuid"], check.Equals, arvadostest.FooCollection)
324 c.Check(gresp.Items[0].(map[string]interface{})["manifest_text"], check.Equals, nil)
328 func (s *CollectionSuite) checkSignatureExpiry(c *check.C, manifestText string, expectedTTL time.Duration) {
329 m := regexp.MustCompile(`@([[:xdigit:]]+)`).FindStringSubmatch(manifestText)
330 c.Assert(m, check.HasLen, 2)
331 sigexp, err := strconv.ParseInt(m[1], 16, 64)
332 c.Assert(err, check.IsNil)
333 expectedExp := time.Now().Add(expectedTTL).Unix()
334 c.Check(sigexp > expectedExp-60, check.Equals, true)
335 c.Check(sigexp <= expectedExp, check.Equals, true)
338 func (s *CollectionSuite) TestSignaturesDisabled(c *check.C) {
339 s.localdb.cluster.Collections.BlobSigning = false
340 ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
342 resp, err := s.localdb.CollectionGet(ctx, arvados.GetOptions{UUID: arvadostest.FooCollection})
343 c.Check(err, check.IsNil)
344 c.Check(resp.ManifestText, check.Matches, `(?ms).* acbd[^ +]*\+3 0:.*`)