9397: only mount points of kind "collection" are allowed under the output_dir.
[arvados.git] / services / crunch-run / crunchrun_test.go
1 package main
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "git.curoverse.com/arvados.git/sdk/go/arvados"
10         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
11         "git.curoverse.com/arvados.git/sdk/go/keepclient"
12         "git.curoverse.com/arvados.git/sdk/go/manifest"
13         "github.com/curoverse/dockerclient"
14         . "gopkg.in/check.v1"
15         "io"
16         "io/ioutil"
17         "os"
18         "os/exec"
19         "path/filepath"
20         "sort"
21         "strings"
22         "sync"
23         "syscall"
24         "testing"
25         "time"
26 )
27
28 // Gocheck boilerplate
29 func TestCrunchExec(t *testing.T) {
30         TestingT(t)
31 }
32
33 type TestSuite struct{}
34
35 // Gocheck boilerplate
36 var _ = Suite(&TestSuite{})
37
38 type ArvTestClient struct {
39         Total   int64
40         Calls   int
41         Content []arvadosclient.Dict
42         arvados.Container
43         Logs          map[string]*bytes.Buffer
44         WasSetRunning bool
45         sync.Mutex
46 }
47
48 type KeepTestClient struct {
49         Called  bool
50         Content []byte
51 }
52
53 var hwManifest = ". 82ab40c24fc8df01798e57ba66795bb1+841216+Aa124ac75e5168396c73c0a18eda641a4f41791c0@569fa8c3 0:841216:9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7.tar\n"
54 var hwPDH = "a45557269dcb65a6b78f9ac061c0850b+120"
55 var hwImageId = "9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7"
56
57 var otherManifest = ". 68a84f561b1d1708c6baff5e019a9ab3+46+Ae5d0af96944a3690becb1decdf60cc1c937f556d@5693216f 0:46:md5sum.txt\n"
58 var otherPDH = "a3e8f74c6f101eae01fa08bfb4e49b3a+54"
59
60 var fakeAuthUUID = "zzzzz-gj3su-55pqoyepgi2glem"
61 var fakeAuthToken = "a3ltuwzqcu2u4sc0q7yhpc2w7s00fdcqecg5d6e0u3pfohmbjt"
62
63 type TestDockerClient struct {
64         imageLoaded string
65         logReader   io.ReadCloser
66         logWriter   io.WriteCloser
67         fn          func(t *TestDockerClient)
68         finish      chan dockerclient.WaitResult
69         stop        chan bool
70         cwd         string
71         env         []string
72         api         *ArvTestClient
73 }
74
75 func NewTestDockerClient() *TestDockerClient {
76         t := &TestDockerClient{}
77         t.logReader, t.logWriter = io.Pipe()
78         t.finish = make(chan dockerclient.WaitResult)
79         t.stop = make(chan bool)
80         t.cwd = "/"
81         return t
82 }
83
84 func (t *TestDockerClient) StopContainer(id string, timeout int) error {
85         t.stop <- true
86         return nil
87 }
88
89 func (t *TestDockerClient) InspectImage(id string) (*dockerclient.ImageInfo, error) {
90         if t.imageLoaded == id {
91                 return &dockerclient.ImageInfo{}, nil
92         } else {
93                 return nil, errors.New("")
94         }
95 }
96
97 func (t *TestDockerClient) LoadImage(reader io.Reader) error {
98         _, err := io.Copy(ioutil.Discard, reader)
99         if err != nil {
100                 return err
101         } else {
102                 t.imageLoaded = hwImageId
103                 return nil
104         }
105 }
106
107 func (t *TestDockerClient) CreateContainer(config *dockerclient.ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (string, error) {
108         if config.WorkingDir != "" {
109                 t.cwd = config.WorkingDir
110         }
111         t.env = config.Env
112         return "abcde", nil
113 }
114
115 func (t *TestDockerClient) StartContainer(id string, config *dockerclient.HostConfig) error {
116         if id == "abcde" {
117                 go t.fn(t)
118                 return nil
119         } else {
120                 return errors.New("Invalid container id")
121         }
122 }
123
124 func (t *TestDockerClient) AttachContainer(id string, options *dockerclient.AttachOptions) (io.ReadCloser, error) {
125         return t.logReader, nil
126 }
127
128 func (t *TestDockerClient) Wait(id string) <-chan dockerclient.WaitResult {
129         return t.finish
130 }
131
132 func (*TestDockerClient) RemoveImage(name string, force bool) ([]*dockerclient.ImageDelete, error) {
133         return nil, nil
134 }
135
136 func (client *ArvTestClient) Create(resourceType string,
137         parameters arvadosclient.Dict,
138         output interface{}) error {
139
140         client.Mutex.Lock()
141         defer client.Mutex.Unlock()
142
143         client.Calls++
144         client.Content = append(client.Content, parameters)
145
146         if resourceType == "logs" {
147                 et := parameters["log"].(arvadosclient.Dict)["event_type"].(string)
148                 if client.Logs == nil {
149                         client.Logs = make(map[string]*bytes.Buffer)
150                 }
151                 if client.Logs[et] == nil {
152                         client.Logs[et] = &bytes.Buffer{}
153                 }
154                 client.Logs[et].Write([]byte(parameters["log"].(arvadosclient.Dict)["properties"].(map[string]string)["text"]))
155         }
156
157         if resourceType == "collections" && output != nil {
158                 mt := parameters["collection"].(arvadosclient.Dict)["manifest_text"].(string)
159                 outmap := output.(*arvados.Collection)
160                 outmap.PortableDataHash = fmt.Sprintf("%x+%d", md5.Sum([]byte(mt)), len(mt))
161         }
162
163         return nil
164 }
165
166 func (client *ArvTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
167         switch {
168         case method == "GET" && resourceType == "containers" && action == "auth":
169                 return json.Unmarshal([]byte(`{
170                         "kind": "arvados#api_client_authorization",
171                         "uuid": "`+fakeAuthUUID+`",
172                         "api_token": "`+fakeAuthToken+`"
173                         }`), output)
174         default:
175                 return fmt.Errorf("Not found")
176         }
177 }
178
179 func (client *ArvTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
180         if resourceType == "collections" {
181                 if uuid == hwPDH {
182                         output.(*arvados.Collection).ManifestText = hwManifest
183                 } else if uuid == otherPDH {
184                         output.(*arvados.Collection).ManifestText = otherManifest
185                 }
186         }
187         if resourceType == "containers" {
188                 (*output.(*arvados.Container)) = client.Container
189         }
190         return nil
191 }
192
193 func (client *ArvTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
194         client.Mutex.Lock()
195         defer client.Mutex.Unlock()
196         client.Calls++
197         client.Content = append(client.Content, parameters)
198         if resourceType == "containers" {
199                 if parameters["container"].(arvadosclient.Dict)["state"] == "Running" {
200                         client.WasSetRunning = true
201                 }
202         }
203         return nil
204 }
205
206 var discoveryMap = map[string]interface{}{"defaultTrashLifetime": float64(1209600)}
207
208 func (client *ArvTestClient) Discovery(key string) (interface{}, error) {
209         return discoveryMap[key], nil
210 }
211
212 // CalledWith returns the parameters from the first API call whose
213 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
214 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
215 // no call matches, it returns nil.
216 func (client *ArvTestClient) CalledWith(jpath string, expect interface{}) arvadosclient.Dict {
217 call:
218         for _, content := range client.Content {
219                 var v interface{} = content
220                 for _, k := range strings.Split(jpath, ".") {
221                         if dict, ok := v.(arvadosclient.Dict); !ok {
222                                 continue call
223                         } else {
224                                 v = dict[k]
225                         }
226                 }
227                 if v == expect {
228                         return content
229                 }
230         }
231         return nil
232 }
233
234 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
235         client.Content = buf
236         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
237 }
238
239 type FileWrapper struct {
240         io.ReadCloser
241         len uint64
242 }
243
244 func (fw FileWrapper) Len() uint64 {
245         return fw.len
246 }
247
248 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
249         if filename == hwImageId+".tar" {
250                 rdr := ioutil.NopCloser(&bytes.Buffer{})
251                 client.Called = true
252                 return FileWrapper{rdr, 1321984}, nil
253         }
254         return nil, nil
255 }
256
257 func (s *TestSuite) TestLoadImage(c *C) {
258         kc := &KeepTestClient{}
259         docker := NewTestDockerClient()
260         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
261
262         _, err := cr.Docker.RemoveImage(hwImageId, true)
263
264         _, err = cr.Docker.InspectImage(hwImageId)
265         c.Check(err, NotNil)
266
267         cr.Container.ContainerImage = hwPDH
268
269         // (1) Test loading image from keep
270         c.Check(kc.Called, Equals, false)
271         c.Check(cr.ContainerConfig.Image, Equals, "")
272
273         err = cr.LoadImage()
274
275         c.Check(err, IsNil)
276         defer func() {
277                 cr.Docker.RemoveImage(hwImageId, true)
278         }()
279
280         c.Check(kc.Called, Equals, true)
281         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
282
283         _, err = cr.Docker.InspectImage(hwImageId)
284         c.Check(err, IsNil)
285
286         // (2) Test using image that's already loaded
287         kc.Called = false
288         cr.ContainerConfig.Image = ""
289
290         err = cr.LoadImage()
291         c.Check(err, IsNil)
292         c.Check(kc.Called, Equals, false)
293         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
294
295 }
296
297 type ArvErrorTestClient struct{}
298
299 func (ArvErrorTestClient) Create(resourceType string,
300         parameters arvadosclient.Dict,
301         output interface{}) error {
302         return nil
303 }
304
305 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
306         return errors.New("ArvError")
307 }
308
309 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
310         return errors.New("ArvError")
311 }
312
313 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
314         return nil
315 }
316
317 func (ArvErrorTestClient) Discovery(key string) (interface{}, error) {
318         return discoveryMap[key], nil
319 }
320
321 type KeepErrorTestClient struct{}
322
323 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
324         return "", 0, errors.New("KeepError")
325 }
326
327 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
328         return nil, errors.New("KeepError")
329 }
330
331 type KeepReadErrorTestClient struct{}
332
333 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
334         return "", 0, nil
335 }
336
337 type ErrorReader struct{}
338
339 func (ErrorReader) Read(p []byte) (n int, err error) {
340         return 0, errors.New("ErrorReader")
341 }
342
343 func (ErrorReader) Close() error {
344         return nil
345 }
346
347 func (ErrorReader) Len() uint64 {
348         return 0
349 }
350
351 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
352         return ErrorReader{}, nil
353 }
354
355 func (s *TestSuite) TestLoadImageArvError(c *C) {
356         // (1) Arvados error
357         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
358         cr.Container.ContainerImage = hwPDH
359
360         err := cr.LoadImage()
361         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
362 }
363
364 func (s *TestSuite) TestLoadImageKeepError(c *C) {
365         // (2) Keep error
366         docker := NewTestDockerClient()
367         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
368         cr.Container.ContainerImage = hwPDH
369
370         err := cr.LoadImage()
371         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
372 }
373
374 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
375         // (3) Collection doesn't contain image
376         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
377         cr.Container.ContainerImage = otherPDH
378
379         err := cr.LoadImage()
380         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
381 }
382
383 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
384         // (4) Collection doesn't contain image
385         docker := NewTestDockerClient()
386         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
387         cr.Container.ContainerImage = hwPDH
388
389         err := cr.LoadImage()
390         c.Check(err, NotNil)
391 }
392
393 type ClosableBuffer struct {
394         bytes.Buffer
395 }
396
397 func (*ClosableBuffer) Close() error {
398         return nil
399 }
400
401 type TestLogs struct {
402         Stdout ClosableBuffer
403         Stderr ClosableBuffer
404 }
405
406 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
407         if logstr == "stdout" {
408                 return &tl.Stdout
409         }
410         if logstr == "stderr" {
411                 return &tl.Stderr
412         }
413         return nil
414 }
415
416 func dockerLog(fd byte, msg string) []byte {
417         by := []byte(msg)
418         header := make([]byte, 8+len(by))
419         header[0] = fd
420         header[7] = byte(len(by))
421         copy(header[8:], by)
422         return header
423 }
424
425 func (s *TestSuite) TestRunContainer(c *C) {
426         docker := NewTestDockerClient()
427         docker.fn = func(t *TestDockerClient) {
428                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
429                 t.logWriter.Close()
430                 t.finish <- dockerclient.WaitResult{}
431         }
432         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
433
434         var logs TestLogs
435         cr.NewLogWriter = logs.NewTestLoggingWriter
436         cr.Container.ContainerImage = hwPDH
437         cr.Container.Command = []string{"./hw"}
438         err := cr.LoadImage()
439         c.Check(err, IsNil)
440
441         err = cr.CreateContainer()
442         c.Check(err, IsNil)
443
444         err = cr.StartContainer()
445         c.Check(err, IsNil)
446
447         err = cr.WaitFinish()
448         c.Check(err, IsNil)
449
450         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
451         c.Check(logs.Stderr.String(), Equals, "")
452 }
453
454 func (s *TestSuite) TestCommitLogs(c *C) {
455         api := &ArvTestClient{}
456         kc := &KeepTestClient{}
457         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
458         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
459
460         cr.CrunchLog.Print("Hello world!")
461         cr.CrunchLog.Print("Goodbye")
462         cr.finalState = "Complete"
463
464         err := cr.CommitLogs()
465         c.Check(err, IsNil)
466
467         c.Check(api.Calls, Equals, 2)
468         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
469         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
470         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
471 }
472
473 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
474         api := &ArvTestClient{}
475         kc := &KeepTestClient{}
476         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
477
478         err := cr.UpdateContainerRunning()
479         c.Check(err, IsNil)
480
481         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
482 }
483
484 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
485         api := &ArvTestClient{}
486         kc := &KeepTestClient{}
487         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
488
489         cr.LogsPDH = new(string)
490         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
491
492         cr.ExitCode = new(int)
493         *cr.ExitCode = 42
494         cr.finalState = "Complete"
495
496         err := cr.UpdateContainerFinal()
497         c.Check(err, IsNil)
498
499         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
500         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
501         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
502 }
503
504 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
505         api := &ArvTestClient{}
506         kc := &KeepTestClient{}
507         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
508         cr.Cancelled = true
509         cr.finalState = "Cancelled"
510
511         err := cr.UpdateContainerFinal()
512         c.Check(err, IsNil)
513
514         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
515         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
516         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
517 }
518
519 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
520 // dress rehearsal of the Run() function, starting from a JSON container record.
521 func FullRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner) {
522         rec := arvados.Container{}
523         err := json.Unmarshal([]byte(record), &rec)
524         c.Check(err, IsNil)
525
526         docker := NewTestDockerClient()
527         docker.fn = fn
528         docker.RemoveImage(hwImageId, true)
529
530         api = &ArvTestClient{Container: rec}
531         docker.api = api
532         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
533         cr.statInterval = 100 * time.Millisecond
534         am := &ArvMountCmdLine{}
535         cr.RunArvMount = am.ArvMountTest
536
537         err = cr.Run()
538         c.Check(err, IsNil)
539         c.Check(api.WasSetRunning, Equals, true)
540
541         c.Check(api.Content[api.Calls-1]["container"].(arvadosclient.Dict)["log"], NotNil)
542
543         if err != nil {
544                 for k, v := range api.Logs {
545                         c.Log(k)
546                         c.Log(v.String())
547                 }
548         }
549
550         return
551 }
552
553 func (s *TestSuite) TestFullRunHello(c *C) {
554         api, _ := FullRunHelper(c, `{
555     "command": ["echo", "hello world"],
556     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
557     "cwd": ".",
558     "environment": {},
559     "mounts": {"/tmp": {"kind": "tmp"} },
560     "output_path": "/tmp",
561     "priority": 1,
562     "runtime_constraints": {}
563 }`, func(t *TestDockerClient) {
564                 t.logWriter.Write(dockerLog(1, "hello world\n"))
565                 t.logWriter.Close()
566                 t.finish <- dockerclient.WaitResult{}
567         })
568
569         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
570         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
571         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
572
573 }
574
575 func (s *TestSuite) TestCrunchstat(c *C) {
576         api, _ := FullRunHelper(c, `{
577                 "command": ["sleep", "1"],
578                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
579                 "cwd": ".",
580                 "environment": {},
581                 "mounts": {"/tmp": {"kind": "tmp"} },
582                 "output_path": "/tmp",
583                 "priority": 1,
584                 "runtime_constraints": {}
585         }`, func(t *TestDockerClient) {
586                 time.Sleep(time.Second)
587                 t.logWriter.Close()
588                 t.finish <- dockerclient.WaitResult{}
589         })
590
591         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
592         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
593
594         // We didn't actually start a container, so crunchstat didn't
595         // find accounting files and therefore didn't log any stats.
596         // It should have logged a "can't find accounting files"
597         // message after one poll interval, though, so we can confirm
598         // it's alive:
599         c.Assert(api.Logs["crunchstat"], NotNil)
600         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files have not appeared after 100ms.*`)
601
602         // The "files never appeared" log assures us that we called
603         // (*crunchstat.Reporter)Stop(), and that we set it up with
604         // the correct container ID "abcde":
605         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files never appeared for abcde\n`)
606 }
607
608 func (s *TestSuite) TestFullRunStderr(c *C) {
609         api, _ := FullRunHelper(c, `{
610     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
611     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
612     "cwd": ".",
613     "environment": {},
614     "mounts": {"/tmp": {"kind": "tmp"} },
615     "output_path": "/tmp",
616     "priority": 1,
617     "runtime_constraints": {}
618 }`, func(t *TestDockerClient) {
619                 t.logWriter.Write(dockerLog(1, "hello\n"))
620                 t.logWriter.Write(dockerLog(2, "world\n"))
621                 t.logWriter.Close()
622                 t.finish <- dockerclient.WaitResult{ExitCode: 1}
623         })
624
625         final := api.CalledWith("container.state", "Complete")
626         c.Assert(final, NotNil)
627         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
628         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
629
630         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
631         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
632 }
633
634 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
635         api, _ := FullRunHelper(c, `{
636     "command": ["pwd"],
637     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
638     "cwd": ".",
639     "environment": {},
640     "mounts": {"/tmp": {"kind": "tmp"} },
641     "output_path": "/tmp",
642     "priority": 1,
643     "runtime_constraints": {}
644 }`, func(t *TestDockerClient) {
645                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
646                 t.logWriter.Close()
647                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
648         })
649
650         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
651         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
652         c.Log(api.Logs["stdout"])
653         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
654 }
655
656 func (s *TestSuite) TestFullRunSetCwd(c *C) {
657         api, _ := FullRunHelper(c, `{
658     "command": ["pwd"],
659     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
660     "cwd": "/bin",
661     "environment": {},
662     "mounts": {"/tmp": {"kind": "tmp"} },
663     "output_path": "/tmp",
664     "priority": 1,
665     "runtime_constraints": {}
666 }`, func(t *TestDockerClient) {
667                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
668                 t.logWriter.Close()
669                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
670         })
671
672         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
673         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
674         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
675 }
676
677 func (s *TestSuite) TestCancel(c *C) {
678         record := `{
679     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
680     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
681     "cwd": ".",
682     "environment": {},
683     "mounts": {"/tmp": {"kind": "tmp"} },
684     "output_path": "/tmp",
685     "priority": 1,
686     "runtime_constraints": {}
687 }`
688
689         rec := arvados.Container{}
690         err := json.Unmarshal([]byte(record), &rec)
691         c.Check(err, IsNil)
692
693         docker := NewTestDockerClient()
694         docker.fn = func(t *TestDockerClient) {
695                 <-t.stop
696                 t.logWriter.Write(dockerLog(1, "foo\n"))
697                 t.logWriter.Close()
698                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
699         }
700         docker.RemoveImage(hwImageId, true)
701
702         api := &ArvTestClient{Container: rec}
703         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
704         am := &ArvMountCmdLine{}
705         cr.RunArvMount = am.ArvMountTest
706
707         go func() {
708                 for cr.ContainerID == "" {
709                         time.Sleep(time.Millisecond)
710                 }
711                 cr.SigChan <- syscall.SIGINT
712         }()
713
714         err = cr.Run()
715
716         c.Check(err, IsNil)
717         if err != nil {
718                 for k, v := range api.Logs {
719                         c.Log(k)
720                         c.Log(v.String())
721                 }
722         }
723
724         c.Check(api.CalledWith("container.log", nil), NotNil)
725         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
726         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
727
728 }
729
730 func (s *TestSuite) TestFullRunSetEnv(c *C) {
731         api, _ := FullRunHelper(c, `{
732     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
733     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
734     "cwd": "/bin",
735     "environment": {"FROBIZ": "bilbo"},
736     "mounts": {"/tmp": {"kind": "tmp"} },
737     "output_path": "/tmp",
738     "priority": 1,
739     "runtime_constraints": {}
740 }`, func(t *TestDockerClient) {
741                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
742                 t.logWriter.Close()
743                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
744         })
745
746         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
747         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
748         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
749 }
750
751 type ArvMountCmdLine struct {
752         Cmd   []string
753         token string
754 }
755
756 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
757         am.Cmd = c
758         am.token = token
759         return nil, nil
760 }
761
762 func stubCert(temp string) string {
763         path := temp + "/ca-certificates.crt"
764         crt, _ := os.Create(path)
765         crt.Close()
766         arvadosclient.CertFiles = []string{path}
767         return path
768 }
769
770 func (s *TestSuite) TestSetupMounts(c *C) {
771         api := &ArvTestClient{}
772         kc := &KeepTestClient{}
773         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
774         am := &ArvMountCmdLine{}
775         cr.RunArvMount = am.ArvMountTest
776
777         realTemp, err := ioutil.TempDir("", "crunchrun_test1-")
778         c.Assert(err, IsNil)
779         certTemp, err := ioutil.TempDir("", "crunchrun_test2-")
780         c.Assert(err, IsNil)
781         stubCertPath := stubCert(certTemp)
782
783         defer os.RemoveAll(realTemp)
784         defer os.RemoveAll(certTemp)
785
786         i := 0
787         cr.MkTempDir = func(_ string, prefix string) (string, error) {
788                 i++
789                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, i)
790                 err := os.Mkdir(d, os.ModePerm)
791                 if err != nil && strings.Contains(err.Error(), ": file exists") {
792                         // Test case must have pre-populated the tempdir
793                         err = nil
794                 }
795                 return d, err
796         }
797
798         checkEmpty := func() {
799                 filepath.Walk(realTemp, func(path string, _ os.FileInfo, err error) error {
800                         c.Check(path, Equals, realTemp)
801                         c.Check(err, IsNil)
802                         return nil
803                 })
804         }
805
806         {
807                 i = 0
808                 cr.Container.Mounts = make(map[string]arvados.Mount)
809                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
810                 cr.OutputPath = "/tmp"
811
812                 err := cr.SetupMounts()
813                 c.Check(err, IsNil)
814                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
815                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp"})
816                 cr.CleanupDirs()
817                 checkEmpty()
818         }
819
820         {
821                 i = 0
822                 cr.Container.Mounts = make(map[string]arvados.Mount)
823                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
824                 cr.OutputPath = "/tmp"
825
826                 apiflag := true
827                 cr.Container.RuntimeConstraints.API = &apiflag
828
829                 err := cr.SetupMounts()
830                 c.Check(err, IsNil)
831                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
832                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", stubCertPath + ":/etc/arvados/ca-certificates.crt:ro"})
833                 cr.CleanupDirs()
834                 checkEmpty()
835
836                 apiflag = false
837         }
838
839         {
840                 i = 0
841                 cr.Container.Mounts = map[string]arvados.Mount{
842                         "/keeptmp": {Kind: "collection", Writable: true},
843                 }
844                 cr.OutputPath = "/keeptmp"
845
846                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
847
848                 err := cr.SetupMounts()
849                 c.Check(err, IsNil)
850                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
851                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
852                 cr.CleanupDirs()
853                 checkEmpty()
854         }
855
856         {
857                 i = 0
858                 cr.Container.Mounts = map[string]arvados.Mount{
859                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
860                         "/keepout": {Kind: "collection", Writable: true},
861                 }
862                 cr.OutputPath = "/keepout"
863
864                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
865                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
866
867                 err := cr.SetupMounts()
868                 c.Check(err, IsNil)
869                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
870                 sort.StringSlice(cr.Binds).Sort()
871                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
872                         realTemp + "/keep1/tmp0:/keepout"})
873                 cr.CleanupDirs()
874                 checkEmpty()
875         }
876
877         {
878                 i = 0
879                 cr.Container.RuntimeConstraints.KeepCacheRAM = 512
880                 cr.Container.Mounts = map[string]arvados.Mount{
881                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
882                         "/keepout": {Kind: "collection", Writable: true},
883                 }
884                 cr.OutputPath = "/keepout"
885
886                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
887                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
888
889                 err := cr.SetupMounts()
890                 c.Check(err, IsNil)
891                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
892                 sort.StringSlice(cr.Binds).Sort()
893                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
894                         realTemp + "/keep1/tmp0:/keepout"})
895                 cr.CleanupDirs()
896                 checkEmpty()
897         }
898
899         for _, test := range []struct {
900                 in  interface{}
901                 out string
902         }{
903                 {in: "foo", out: `"foo"`},
904                 {in: nil, out: `null`},
905                 {in: map[string]int{"foo": 123}, out: `{"foo":123}`},
906         } {
907                 i = 0
908                 cr.Container.Mounts = map[string]arvados.Mount{
909                         "/mnt/test.json": {Kind: "json", Content: test.in},
910                 }
911                 err := cr.SetupMounts()
912                 c.Check(err, IsNil)
913                 sort.StringSlice(cr.Binds).Sort()
914                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
915                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
916                 c.Check(err, IsNil)
917                 c.Check(content, DeepEquals, []byte(test.out))
918                 cr.CleanupDirs()
919                 checkEmpty()
920         }
921
922         // Read-only mount points are allowed underneath output_dir mount point
923         {
924                 i = 0
925                 cr.Container.Mounts = make(map[string]arvados.Mount)
926                 cr.Container.Mounts = map[string]arvados.Mount{
927                         "/tmp":     {Kind: "tmp"},
928                         "/tmp/foo": {Kind: "collection"},
929                 }
930                 cr.OutputPath = "/tmp"
931
932                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
933
934                 err := cr.SetupMounts()
935                 c.Check(err, IsNil)
936                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
937                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", realTemp + "/keep1/tmp0:/tmp/foo:ro"})
938                 cr.CleanupDirs()
939                 checkEmpty()
940         }
941
942         // Writable mount points are not allowed underneath output_dir mount point
943         {
944                 i = 0
945                 cr.Container.Mounts = make(map[string]arvados.Mount)
946                 cr.Container.Mounts = map[string]arvados.Mount{
947                         "/tmp":     {Kind: "tmp"},
948                         "/tmp/foo": {Kind: "collection", Writable: true},
949                 }
950                 cr.OutputPath = "/tmp"
951
952                 err := cr.SetupMounts()
953                 c.Check(err, NotNil)
954                 c.Check(err, ErrorMatches, `Writable mount points are not permitted underneath the output_path.*`)
955                 cr.CleanupDirs()
956                 checkEmpty()
957         }
958
959         // Only mount points of kind 'collection' are allowed underneath output_dir mount point
960         {
961                 i = 0
962                 cr.Container.Mounts = make(map[string]arvados.Mount)
963                 cr.Container.Mounts = map[string]arvados.Mount{
964                         "/tmp":     {Kind: "tmp"},
965                         "/tmp/foo": {Kind: "json"},
966                 }
967                 cr.OutputPath = "/tmp"
968
969                 err := cr.SetupMounts()
970                 c.Check(err, NotNil)
971                 c.Check(err, ErrorMatches, `Only mount points of kind 'collection' are supported underneath the output_path.*`)
972                 cr.CleanupDirs()
973                 checkEmpty()
974         }
975 }
976
977 func (s *TestSuite) TestStdout(c *C) {
978         helperRecord := `{
979                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
980                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
981                 "cwd": "/bin",
982                 "environment": {"FROBIZ": "bilbo"},
983                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
984                 "output_path": "/tmp",
985                 "priority": 1,
986                 "runtime_constraints": {}
987         }`
988
989         api, _ := FullRunHelper(c, helperRecord, func(t *TestDockerClient) {
990                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
991                 t.logWriter.Close()
992                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
993         })
994
995         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
996         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
997         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
998 }
999
1000 // Used by the TestStdoutWithWrongPath*()
1001 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
1002         rec := arvados.Container{}
1003         err = json.Unmarshal([]byte(record), &rec)
1004         c.Check(err, IsNil)
1005
1006         docker := NewTestDockerClient()
1007         docker.fn = fn
1008         docker.RemoveImage(hwImageId, true)
1009
1010         api = &ArvTestClient{Container: rec}
1011         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1012         am := &ArvMountCmdLine{}
1013         cr.RunArvMount = am.ArvMountTest
1014
1015         err = cr.Run()
1016         return
1017 }
1018
1019 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
1020         _, _, err := StdoutErrorRunHelper(c, `{
1021     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
1022     "output_path": "/tmp"
1023 }`, func(t *TestDockerClient) {})
1024
1025         c.Check(err, NotNil)
1026         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
1027 }
1028
1029 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
1030         _, _, err := StdoutErrorRunHelper(c, `{
1031     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
1032     "output_path": "/tmp"
1033 }`, func(t *TestDockerClient) {})
1034
1035         c.Check(err, NotNil)
1036         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
1037 }
1038
1039 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
1040         _, _, err := StdoutErrorRunHelper(c, `{
1041     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
1042     "output_path": "/tmp"
1043 }`, func(t *TestDockerClient) {})
1044
1045         c.Check(err, NotNil)
1046         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
1047 }
1048
1049 func (s *TestSuite) TestFullRunWithAPI(c *C) {
1050         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1051         defer os.Unsetenv("ARVADOS_API_HOST")
1052         api, _ := FullRunHelper(c, `{
1053     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1054     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1055     "cwd": "/bin",
1056     "environment": {},
1057     "mounts": {"/tmp": {"kind": "tmp"} },
1058     "output_path": "/tmp",
1059     "priority": 1,
1060     "runtime_constraints": {"API": true}
1061 }`, func(t *TestDockerClient) {
1062                 t.logWriter.Write(dockerLog(1, t.env[1][17:]+"\n"))
1063                 t.logWriter.Close()
1064                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1065         })
1066
1067         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1068         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1069         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "test.arvados.org\n"), Equals, true)
1070         c.Check(api.CalledWith("container.output", "d41d8cd98f00b204e9800998ecf8427e+0"), NotNil)
1071 }
1072
1073 func (s *TestSuite) TestFullRunSetOutput(c *C) {
1074         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1075         defer os.Unsetenv("ARVADOS_API_HOST")
1076         api, _ := FullRunHelper(c, `{
1077     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1078     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1079     "cwd": "/bin",
1080     "environment": {},
1081     "mounts": {"/tmp": {"kind": "tmp"} },
1082     "output_path": "/tmp",
1083     "priority": 1,
1084     "runtime_constraints": {"API": true}
1085 }`, func(t *TestDockerClient) {
1086                 t.api.Container.Output = "d4ab34d3d4f8a72f5c4973051ae69fab+122"
1087                 t.logWriter.Close()
1088                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1089         })
1090
1091         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1092         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1093         c.Check(api.CalledWith("container.output", "d4ab34d3d4f8a72f5c4973051ae69fab+122"), NotNil)
1094 }