Merge branch '12574-propagate-priority' closes #12574
[arvados.git] / services / crunch-run / crunchrun_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bufio"
9         "bytes"
10         "context"
11         "crypto/md5"
12         "encoding/json"
13         "errors"
14         "fmt"
15         "io"
16         "io/ioutil"
17         "net"
18         "os"
19         "os/exec"
20         "path/filepath"
21         "runtime/pprof"
22         "sort"
23         "strings"
24         "sync"
25         "syscall"
26         "testing"
27         "time"
28
29         "git.curoverse.com/arvados.git/sdk/go/arvados"
30         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
31         "git.curoverse.com/arvados.git/sdk/go/manifest"
32
33         dockertypes "github.com/docker/docker/api/types"
34         dockercontainer "github.com/docker/docker/api/types/container"
35         dockernetwork "github.com/docker/docker/api/types/network"
36         . "gopkg.in/check.v1"
37 )
38
39 // Gocheck boilerplate
40 func TestCrunchExec(t *testing.T) {
41         TestingT(t)
42 }
43
44 type TestSuite struct{}
45
46 // Gocheck boilerplate
47 var _ = Suite(&TestSuite{})
48
49 type ArvTestClient struct {
50         Total   int64
51         Calls   int
52         Content []arvadosclient.Dict
53         arvados.Container
54         Logs map[string]*bytes.Buffer
55         sync.Mutex
56         WasSetRunning bool
57         callraw       bool
58 }
59
60 type KeepTestClient struct {
61         Called  bool
62         Content []byte
63 }
64
65 var hwManifest = ". 82ab40c24fc8df01798e57ba66795bb1+841216+Aa124ac75e5168396c73c0a18eda641a4f41791c0@569fa8c3 0:841216:9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7.tar\n"
66 var hwPDH = "a45557269dcb65a6b78f9ac061c0850b+120"
67 var hwImageId = "9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7"
68
69 var otherManifest = ". 68a84f561b1d1708c6baff5e019a9ab3+46+Ae5d0af96944a3690becb1decdf60cc1c937f556d@5693216f 0:46:md5sum.txt\n"
70 var otherPDH = "a3e8f74c6f101eae01fa08bfb4e49b3a+54"
71
72 var normalizedManifestWithSubdirs = `. 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 0:9:file1_in_main.txt 9:18:file2_in_main.txt 0:27:zzzzz-8i9sb-bcdefghijkdhvnk.log.txt
73 ./subdir1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
74 ./subdir1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
75 `
76
77 var normalizedWithSubdirsPDH = "a0def87f80dd594d4675809e83bd4f15+367"
78
79 var denormalizedManifestWithSubdirs = ". 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 0:9:file1_in_main.txt 9:18:file2_in_main.txt 0:27:zzzzz-8i9sb-bcdefghijkdhvnk.log.txt 0:10:subdir1/file1_in_subdir1.txt 10:17:subdir1/file2_in_subdir1.txt\n"
80 var denormalizedWithSubdirsPDH = "b0def87f80dd594d4675809e83bd4f15+367"
81
82 var fakeAuthUUID = "zzzzz-gj3su-55pqoyepgi2glem"
83 var fakeAuthToken = "a3ltuwzqcu2u4sc0q7yhpc2w7s00fdcqecg5d6e0u3pfohmbjt"
84
85 type TestDockerClient struct {
86         imageLoaded string
87         logReader   io.ReadCloser
88         logWriter   io.WriteCloser
89         fn          func(t *TestDockerClient)
90         finish      int
91         stop        chan bool
92         cwd         string
93         env         []string
94         api         *ArvTestClient
95         realTemp    string
96 }
97
98 func NewTestDockerClient(exitCode int) *TestDockerClient {
99         t := &TestDockerClient{}
100         t.logReader, t.logWriter = io.Pipe()
101         t.finish = exitCode
102         t.stop = make(chan bool, 1)
103         t.cwd = "/"
104         return t
105 }
106
107 type MockConn struct {
108         net.Conn
109 }
110
111 func (m *MockConn) Write(b []byte) (int, error) {
112         return len(b), nil
113 }
114
115 func NewMockConn() *MockConn {
116         c := &MockConn{}
117         return c
118 }
119
120 func (t *TestDockerClient) ContainerAttach(ctx context.Context, container string, options dockertypes.ContainerAttachOptions) (dockertypes.HijackedResponse, error) {
121         return dockertypes.HijackedResponse{Conn: NewMockConn(), Reader: bufio.NewReader(t.logReader)}, nil
122 }
123
124 func (t *TestDockerClient) ContainerCreate(ctx context.Context, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, networkingConfig *dockernetwork.NetworkingConfig, containerName string) (dockercontainer.ContainerCreateCreatedBody, error) {
125         if config.WorkingDir != "" {
126                 t.cwd = config.WorkingDir
127         }
128         t.env = config.Env
129         return dockercontainer.ContainerCreateCreatedBody{ID: "abcde"}, nil
130 }
131
132 func (t *TestDockerClient) ContainerStart(ctx context.Context, container string, options dockertypes.ContainerStartOptions) error {
133         if container == "abcde" {
134                 // t.fn gets executed in ContainerWait
135                 return nil
136         } else {
137                 return errors.New("Invalid container id")
138         }
139 }
140
141 func (t *TestDockerClient) ContainerStop(ctx context.Context, container string, timeout *time.Duration) error {
142         t.stop <- true
143         return nil
144 }
145
146 func (t *TestDockerClient) ContainerWait(ctx context.Context, container string, condition dockercontainer.WaitCondition) (<-chan dockercontainer.ContainerWaitOKBody, <-chan error) {
147         body := make(chan dockercontainer.ContainerWaitOKBody)
148         err := make(chan error)
149         go func() {
150                 t.fn(t)
151                 body <- dockercontainer.ContainerWaitOKBody{StatusCode: int64(t.finish)}
152                 close(body)
153                 close(err)
154         }()
155         return body, err
156 }
157
158 func (t *TestDockerClient) ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error) {
159         if t.finish == 2 {
160                 return dockertypes.ImageInspect{}, nil, fmt.Errorf("Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?")
161         }
162
163         if t.imageLoaded == image {
164                 return dockertypes.ImageInspect{}, nil, nil
165         } else {
166                 return dockertypes.ImageInspect{}, nil, errors.New("")
167         }
168 }
169
170 func (t *TestDockerClient) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (dockertypes.ImageLoadResponse, error) {
171         if t.finish == 2 {
172                 return dockertypes.ImageLoadResponse{}, fmt.Errorf("Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?")
173         }
174         _, err := io.Copy(ioutil.Discard, input)
175         if err != nil {
176                 return dockertypes.ImageLoadResponse{}, err
177         } else {
178                 t.imageLoaded = hwImageId
179                 return dockertypes.ImageLoadResponse{Body: ioutil.NopCloser(input)}, nil
180         }
181 }
182
183 func (*TestDockerClient) ImageRemove(ctx context.Context, image string, options dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) {
184         return nil, nil
185 }
186
187 func (client *ArvTestClient) Create(resourceType string,
188         parameters arvadosclient.Dict,
189         output interface{}) error {
190
191         client.Mutex.Lock()
192         defer client.Mutex.Unlock()
193
194         client.Calls++
195         client.Content = append(client.Content, parameters)
196
197         if resourceType == "logs" {
198                 et := parameters["log"].(arvadosclient.Dict)["event_type"].(string)
199                 if client.Logs == nil {
200                         client.Logs = make(map[string]*bytes.Buffer)
201                 }
202                 if client.Logs[et] == nil {
203                         client.Logs[et] = &bytes.Buffer{}
204                 }
205                 client.Logs[et].Write([]byte(parameters["log"].(arvadosclient.Dict)["properties"].(map[string]string)["text"]))
206         }
207
208         if resourceType == "collections" && output != nil {
209                 mt := parameters["collection"].(arvadosclient.Dict)["manifest_text"].(string)
210                 outmap := output.(*arvados.Collection)
211                 outmap.PortableDataHash = fmt.Sprintf("%x+%d", md5.Sum([]byte(mt)), len(mt))
212         }
213
214         return nil
215 }
216
217 func (client *ArvTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
218         switch {
219         case method == "GET" && resourceType == "containers" && action == "auth":
220                 return json.Unmarshal([]byte(`{
221                         "kind": "arvados#api_client_authorization",
222                         "uuid": "`+fakeAuthUUID+`",
223                         "api_token": "`+fakeAuthToken+`"
224                         }`), output)
225         default:
226                 return fmt.Errorf("Not found")
227         }
228 }
229
230 func (client *ArvTestClient) CallRaw(method, resourceType, uuid, action string,
231         parameters arvadosclient.Dict) (reader io.ReadCloser, err error) {
232         var j []byte
233         if method == "GET" && resourceType == "containers" && action == "" && !client.callraw {
234                 j, err = json.Marshal(client.Container)
235         } else {
236                 j = []byte(`{
237                         "command": ["sleep", "1"],
238                         "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
239                         "cwd": ".",
240                         "environment": {},
241                         "mounts": {"/tmp": {"kind": "tmp"}, "/json": {"kind": "json", "content": {"number": 123456789123456789}}},
242                         "output_path": "/tmp",
243                         "priority": 1,
244                         "runtime_constraints": {}
245                 }`)
246         }
247         return ioutil.NopCloser(bytes.NewReader(j)), err
248 }
249
250 func (client *ArvTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
251         if resourceType == "collections" {
252                 if uuid == hwPDH {
253                         output.(*arvados.Collection).ManifestText = hwManifest
254                 } else if uuid == otherPDH {
255                         output.(*arvados.Collection).ManifestText = otherManifest
256                 } else if uuid == normalizedWithSubdirsPDH {
257                         output.(*arvados.Collection).ManifestText = normalizedManifestWithSubdirs
258                 } else if uuid == denormalizedWithSubdirsPDH {
259                         output.(*arvados.Collection).ManifestText = denormalizedManifestWithSubdirs
260                 }
261         }
262         if resourceType == "containers" {
263                 (*output.(*arvados.Container)) = client.Container
264         }
265         return nil
266 }
267
268 func (client *ArvTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
269         client.Mutex.Lock()
270         defer client.Mutex.Unlock()
271         client.Calls++
272         client.Content = append(client.Content, parameters)
273         if resourceType == "containers" {
274                 if parameters["container"].(arvadosclient.Dict)["state"] == "Running" {
275                         client.WasSetRunning = true
276                 }
277         }
278         return nil
279 }
280
281 var discoveryMap = map[string]interface{}{
282         "defaultTrashLifetime":               float64(1209600),
283         "crunchLimitLogBytesPerJob":          float64(67108864),
284         "crunchLogThrottleBytes":             float64(65536),
285         "crunchLogThrottlePeriod":            float64(60),
286         "crunchLogThrottleLines":             float64(1024),
287         "crunchLogPartialLineThrottlePeriod": float64(5),
288         "crunchLogBytesPerEvent":             float64(4096),
289         "crunchLogSecondsBetweenEvents":      float64(1),
290 }
291
292 func (client *ArvTestClient) Discovery(key string) (interface{}, error) {
293         return discoveryMap[key], nil
294 }
295
296 // CalledWith returns the parameters from the first API call whose
297 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
298 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
299 // no call matches, it returns nil.
300 func (client *ArvTestClient) CalledWith(jpath string, expect interface{}) arvadosclient.Dict {
301 call:
302         for _, content := range client.Content {
303                 var v interface{} = content
304                 for _, k := range strings.Split(jpath, ".") {
305                         if dict, ok := v.(arvadosclient.Dict); !ok {
306                                 continue call
307                         } else {
308                                 v = dict[k]
309                         }
310                 }
311                 if v == expect {
312                         return content
313                 }
314         }
315         return nil
316 }
317
318 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
319         client.Content = buf
320         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
321 }
322
323 func (*KeepTestClient) ClearBlockCache() {
324 }
325
326 type FileWrapper struct {
327         io.ReadCloser
328         len int64
329 }
330
331 func (fw FileWrapper) Readdir(n int) ([]os.FileInfo, error) {
332         return nil, errors.New("not implemented")
333 }
334
335 func (fw FileWrapper) Seek(int64, int) (int64, error) {
336         return 0, errors.New("not implemented")
337 }
338
339 func (fw FileWrapper) Size() int64 {
340         return fw.len
341 }
342
343 func (fw FileWrapper) Stat() (os.FileInfo, error) {
344         return nil, errors.New("not implemented")
345 }
346
347 func (fw FileWrapper) Truncate(int64) error {
348         return errors.New("not implemented")
349 }
350
351 func (fw FileWrapper) Write([]byte) (int, error) {
352         return 0, errors.New("not implemented")
353 }
354
355 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
356         if filename == hwImageId+".tar" {
357                 rdr := ioutil.NopCloser(&bytes.Buffer{})
358                 client.Called = true
359                 return FileWrapper{rdr, 1321984}, nil
360         } else if filename == "/file1_in_main.txt" {
361                 rdr := ioutil.NopCloser(strings.NewReader("foo"))
362                 client.Called = true
363                 return FileWrapper{rdr, 3}, nil
364         }
365         return nil, nil
366 }
367
368 func (s *TestSuite) TestLoadImage(c *C) {
369         kc := &KeepTestClient{}
370         docker := NewTestDockerClient(0)
371         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
372
373         _, err := cr.Docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
374
375         _, _, err = cr.Docker.ImageInspectWithRaw(nil, hwImageId)
376         c.Check(err, NotNil)
377
378         cr.Container.ContainerImage = hwPDH
379
380         // (1) Test loading image from keep
381         c.Check(kc.Called, Equals, false)
382         c.Check(cr.ContainerConfig.Image, Equals, "")
383
384         err = cr.LoadImage()
385
386         c.Check(err, IsNil)
387         defer func() {
388                 cr.Docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
389         }()
390
391         c.Check(kc.Called, Equals, true)
392         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
393
394         _, _, err = cr.Docker.ImageInspectWithRaw(nil, hwImageId)
395         c.Check(err, IsNil)
396
397         // (2) Test using image that's already loaded
398         kc.Called = false
399         cr.ContainerConfig.Image = ""
400
401         err = cr.LoadImage()
402         c.Check(err, IsNil)
403         c.Check(kc.Called, Equals, false)
404         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
405
406 }
407
408 type ArvErrorTestClient struct{}
409
410 func (ArvErrorTestClient) Create(resourceType string,
411         parameters arvadosclient.Dict,
412         output interface{}) error {
413         return nil
414 }
415
416 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
417         return errors.New("ArvError")
418 }
419
420 func (ArvErrorTestClient) CallRaw(method, resourceType, uuid, action string,
421         parameters arvadosclient.Dict) (reader io.ReadCloser, err error) {
422         return nil, errors.New("ArvError")
423 }
424
425 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
426         return errors.New("ArvError")
427 }
428
429 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
430         return nil
431 }
432
433 func (ArvErrorTestClient) Discovery(key string) (interface{}, error) {
434         return discoveryMap[key], nil
435 }
436
437 type KeepErrorTestClient struct{}
438
439 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
440         return "", 0, errors.New("KeepError")
441 }
442
443 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
444         return nil, errors.New("KeepError")
445 }
446
447 func (KeepErrorTestClient) ClearBlockCache() {
448 }
449
450 type KeepReadErrorTestClient struct{}
451
452 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
453         return "", 0, nil
454 }
455
456 func (KeepReadErrorTestClient) ClearBlockCache() {
457 }
458
459 type ErrorReader struct {
460         FileWrapper
461 }
462
463 func (ErrorReader) Read(p []byte) (n int, err error) {
464         return 0, errors.New("ErrorReader")
465 }
466
467 func (ErrorReader) Seek(int64, int) (int64, error) {
468         return 0, errors.New("ErrorReader")
469 }
470
471 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
472         return ErrorReader{}, nil
473 }
474
475 func (s *TestSuite) TestLoadImageArvError(c *C) {
476         // (1) Arvados error
477         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
478         cr.Container.ContainerImage = hwPDH
479
480         err := cr.LoadImage()
481         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
482 }
483
484 func (s *TestSuite) TestLoadImageKeepError(c *C) {
485         // (2) Keep error
486         docker := NewTestDockerClient(0)
487         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
488         cr.Container.ContainerImage = hwPDH
489
490         err := cr.LoadImage()
491         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
492 }
493
494 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
495         // (3) Collection doesn't contain image
496         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
497         cr.Container.ContainerImage = otherPDH
498
499         err := cr.LoadImage()
500         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
501 }
502
503 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
504         // (4) Collection doesn't contain image
505         docker := NewTestDockerClient(0)
506         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
507         cr.Container.ContainerImage = hwPDH
508
509         err := cr.LoadImage()
510         c.Check(err, NotNil)
511 }
512
513 type ClosableBuffer struct {
514         bytes.Buffer
515 }
516
517 func (*ClosableBuffer) Close() error {
518         return nil
519 }
520
521 type TestLogs struct {
522         Stdout ClosableBuffer
523         Stderr ClosableBuffer
524 }
525
526 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
527         if logstr == "stdout" {
528                 return &tl.Stdout
529         }
530         if logstr == "stderr" {
531                 return &tl.Stderr
532         }
533         return nil
534 }
535
536 func dockerLog(fd byte, msg string) []byte {
537         by := []byte(msg)
538         header := make([]byte, 8+len(by))
539         header[0] = fd
540         header[7] = byte(len(by))
541         copy(header[8:], by)
542         return header
543 }
544
545 func (s *TestSuite) TestRunContainer(c *C) {
546         docker := NewTestDockerClient(0)
547         docker.fn = func(t *TestDockerClient) {
548                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
549                 t.logWriter.Close()
550         }
551         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
552
553         var logs TestLogs
554         cr.NewLogWriter = logs.NewTestLoggingWriter
555         cr.Container.ContainerImage = hwPDH
556         cr.Container.Command = []string{"./hw"}
557         err := cr.LoadImage()
558         c.Check(err, IsNil)
559
560         err = cr.CreateContainer()
561         c.Check(err, IsNil)
562
563         err = cr.StartContainer()
564         c.Check(err, IsNil)
565
566         err = cr.WaitFinish()
567         c.Check(err, IsNil)
568
569         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
570         c.Check(logs.Stderr.String(), Equals, "")
571 }
572
573 func (s *TestSuite) TestCommitLogs(c *C) {
574         api := &ArvTestClient{}
575         kc := &KeepTestClient{}
576         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
577         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
578
579         cr.CrunchLog.Print("Hello world!")
580         cr.CrunchLog.Print("Goodbye")
581         cr.finalState = "Complete"
582
583         err := cr.CommitLogs()
584         c.Check(err, IsNil)
585
586         c.Check(api.Calls, Equals, 2)
587         c.Check(api.Content[1]["ensure_unique_name"], Equals, true)
588         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
589         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
590         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
591 }
592
593 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
594         api := &ArvTestClient{}
595         kc := &KeepTestClient{}
596         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
597
598         err := cr.UpdateContainerRunning()
599         c.Check(err, IsNil)
600
601         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
602 }
603
604 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
605         api := &ArvTestClient{}
606         kc := &KeepTestClient{}
607         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
608
609         cr.LogsPDH = new(string)
610         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
611
612         cr.ExitCode = new(int)
613         *cr.ExitCode = 42
614         cr.finalState = "Complete"
615
616         err := cr.UpdateContainerFinal()
617         c.Check(err, IsNil)
618
619         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
620         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
621         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
622 }
623
624 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
625         api := &ArvTestClient{}
626         kc := &KeepTestClient{}
627         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
628         cr.cCancelled = true
629         cr.finalState = "Cancelled"
630
631         err := cr.UpdateContainerFinal()
632         c.Check(err, IsNil)
633
634         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
635         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
636         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
637 }
638
639 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
640 // dress rehearsal of the Run() function, starting from a JSON container record.
641 func FullRunHelper(c *C, record string, extraMounts []string, exitCode int, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, realTemp string) {
642         rec := arvados.Container{}
643         err := json.Unmarshal([]byte(record), &rec)
644         c.Check(err, IsNil)
645
646         docker := NewTestDockerClient(exitCode)
647         docker.fn = fn
648         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
649
650         api = &ArvTestClient{Container: rec}
651         docker.api = api
652         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
653         cr.statInterval = 100 * time.Millisecond
654         am := &ArvMountCmdLine{}
655         cr.RunArvMount = am.ArvMountTest
656
657         realTemp, err = ioutil.TempDir("", "crunchrun_test1-")
658         c.Assert(err, IsNil)
659         defer os.RemoveAll(realTemp)
660
661         docker.realTemp = realTemp
662
663         tempcount := 0
664         cr.MkTempDir = func(_ string, prefix string) (string, error) {
665                 tempcount++
666                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, tempcount)
667                 err := os.Mkdir(d, os.ModePerm)
668                 if err != nil && strings.Contains(err.Error(), ": file exists") {
669                         // Test case must have pre-populated the tempdir
670                         err = nil
671                 }
672                 return d, err
673         }
674
675         if extraMounts != nil && len(extraMounts) > 0 {
676                 err := cr.SetupArvMountPoint("keep")
677                 c.Check(err, IsNil)
678
679                 for _, m := range extraMounts {
680                         os.MkdirAll(cr.ArvMountPoint+"/by_id/"+m, os.ModePerm)
681                 }
682         }
683
684         err = cr.Run()
685         if api.CalledWith("container.state", "Complete") != nil {
686                 c.Check(err, IsNil)
687         }
688         if exitCode != 2 {
689                 c.Check(api.WasSetRunning, Equals, true)
690                 c.Check(api.Content[api.Calls-2]["container"].(arvadosclient.Dict)["log"], NotNil)
691         }
692
693         if err != nil {
694                 for k, v := range api.Logs {
695                         c.Log(k)
696                         c.Log(v.String())
697                 }
698         }
699
700         return
701 }
702
703 func (s *TestSuite) TestFullRunHello(c *C) {
704         api, _, _ := FullRunHelper(c, `{
705     "command": ["echo", "hello world"],
706     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
707     "cwd": ".",
708     "environment": {},
709     "mounts": {"/tmp": {"kind": "tmp"} },
710     "output_path": "/tmp",
711     "priority": 1,
712     "runtime_constraints": {}
713 }`, nil, 0, func(t *TestDockerClient) {
714                 t.logWriter.Write(dockerLog(1, "hello world\n"))
715                 t.logWriter.Close()
716         })
717
718         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
719         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
720         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
721
722 }
723
724 func (s *TestSuite) TestCrunchstat(c *C) {
725         api, _, _ := FullRunHelper(c, `{
726                 "command": ["sleep", "1"],
727                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
728                 "cwd": ".",
729                 "environment": {},
730                 "mounts": {"/tmp": {"kind": "tmp"} },
731                 "output_path": "/tmp",
732                 "priority": 1,
733                 "runtime_constraints": {}
734         }`, nil, 0, func(t *TestDockerClient) {
735                 time.Sleep(time.Second)
736                 t.logWriter.Close()
737         })
738
739         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
740         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
741
742         // We didn't actually start a container, so crunchstat didn't
743         // find accounting files and therefore didn't log any stats.
744         // It should have logged a "can't find accounting files"
745         // message after one poll interval, though, so we can confirm
746         // it's alive:
747         c.Assert(api.Logs["crunchstat"], NotNil)
748         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files have not appeared after 100ms.*`)
749
750         // The "files never appeared" log assures us that we called
751         // (*crunchstat.Reporter)Stop(), and that we set it up with
752         // the correct container ID "abcde":
753         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files never appeared for abcde\n`)
754 }
755
756 func (s *TestSuite) TestNodeInfoLog(c *C) {
757         api, _, _ := FullRunHelper(c, `{
758                 "command": ["sleep", "1"],
759                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
760                 "cwd": ".",
761                 "environment": {},
762                 "mounts": {"/tmp": {"kind": "tmp"} },
763                 "output_path": "/tmp",
764                 "priority": 1,
765                 "runtime_constraints": {}
766         }`, nil, 0,
767                 func(t *TestDockerClient) {
768                         time.Sleep(time.Second)
769                         t.logWriter.Close()
770                 })
771
772         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
773         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
774
775         c.Assert(api.Logs["node-info"], NotNil)
776         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Host Information.*`)
777         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*CPU Information.*`)
778         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Memory Information.*`)
779         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Disk Space.*`)
780         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Disk INodes.*`)
781 }
782
783 func (s *TestSuite) TestContainerRecordLog(c *C) {
784         api, _, _ := FullRunHelper(c, `{
785                 "command": ["sleep", "1"],
786                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
787                 "cwd": ".",
788                 "environment": {},
789                 "mounts": {"/tmp": {"kind": "tmp"} },
790                 "output_path": "/tmp",
791                 "priority": 1,
792                 "runtime_constraints": {}
793         }`, nil, 0,
794                 func(t *TestDockerClient) {
795                         time.Sleep(time.Second)
796                         t.logWriter.Close()
797                 })
798
799         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
800         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
801
802         c.Assert(api.Logs["container"], NotNil)
803         c.Check(api.Logs["container"].String(), Matches, `(?ms).*container_image.*`)
804 }
805
806 func (s *TestSuite) TestFullRunStderr(c *C) {
807         api, _, _ := FullRunHelper(c, `{
808     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
809     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
810     "cwd": ".",
811     "environment": {},
812     "mounts": {"/tmp": {"kind": "tmp"} },
813     "output_path": "/tmp",
814     "priority": 1,
815     "runtime_constraints": {}
816 }`, nil, 1, func(t *TestDockerClient) {
817                 t.logWriter.Write(dockerLog(1, "hello\n"))
818                 t.logWriter.Write(dockerLog(2, "world\n"))
819                 t.logWriter.Close()
820         })
821
822         final := api.CalledWith("container.state", "Complete")
823         c.Assert(final, NotNil)
824         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
825         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
826
827         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
828         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
829 }
830
831 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
832         api, _, _ := FullRunHelper(c, `{
833     "command": ["pwd"],
834     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
835     "cwd": ".",
836     "environment": {},
837     "mounts": {"/tmp": {"kind": "tmp"} },
838     "output_path": "/tmp",
839     "priority": 1,
840     "runtime_constraints": {}
841 }`, nil, 0, func(t *TestDockerClient) {
842                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
843                 t.logWriter.Close()
844         })
845
846         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
847         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
848         c.Log(api.Logs["stdout"])
849         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
850 }
851
852 func (s *TestSuite) TestFullRunSetCwd(c *C) {
853         api, _, _ := FullRunHelper(c, `{
854     "command": ["pwd"],
855     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
856     "cwd": "/bin",
857     "environment": {},
858     "mounts": {"/tmp": {"kind": "tmp"} },
859     "output_path": "/tmp",
860     "priority": 1,
861     "runtime_constraints": {}
862 }`, nil, 0, func(t *TestDockerClient) {
863                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
864                 t.logWriter.Close()
865         })
866
867         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
868         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
869         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
870 }
871
872 func (s *TestSuite) TestStopOnSignal(c *C) {
873         s.testStopContainer(c, func(cr *ContainerRunner) {
874                 go func() {
875                         for !cr.cStarted {
876                                 time.Sleep(time.Millisecond)
877                         }
878                         cr.SigChan <- syscall.SIGINT
879                 }()
880         })
881 }
882
883 func (s *TestSuite) TestStopOnArvMountDeath(c *C) {
884         s.testStopContainer(c, func(cr *ContainerRunner) {
885                 cr.ArvMountExit = make(chan error)
886                 go func() {
887                         cr.ArvMountExit <- exec.Command("true").Run()
888                         close(cr.ArvMountExit)
889                 }()
890         })
891 }
892
893 func (s *TestSuite) testStopContainer(c *C, setup func(cr *ContainerRunner)) {
894         record := `{
895     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
896     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
897     "cwd": ".",
898     "environment": {},
899     "mounts": {"/tmp": {"kind": "tmp"} },
900     "output_path": "/tmp",
901     "priority": 1,
902     "runtime_constraints": {}
903 }`
904
905         rec := arvados.Container{}
906         err := json.Unmarshal([]byte(record), &rec)
907         c.Check(err, IsNil)
908
909         docker := NewTestDockerClient(0)
910         docker.fn = func(t *TestDockerClient) {
911                 <-t.stop
912                 t.logWriter.Write(dockerLog(1, "foo\n"))
913                 t.logWriter.Close()
914         }
915         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
916
917         api := &ArvTestClient{Container: rec}
918         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
919         cr.RunArvMount = func([]string, string) (*exec.Cmd, error) { return nil, nil }
920         setup(cr)
921
922         done := make(chan error)
923         go func() {
924                 done <- cr.Run()
925         }()
926         select {
927         case <-time.After(20 * time.Second):
928                 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
929                 c.Fatal("timed out")
930         case err = <-done:
931                 c.Check(err, IsNil)
932         }
933         for k, v := range api.Logs {
934                 c.Log(k)
935                 c.Log(v.String())
936         }
937
938         c.Check(api.CalledWith("container.log", nil), NotNil)
939         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
940         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
941 }
942
943 func (s *TestSuite) TestFullRunSetEnv(c *C) {
944         api, _, _ := FullRunHelper(c, `{
945     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
946     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
947     "cwd": "/bin",
948     "environment": {"FROBIZ": "bilbo"},
949     "mounts": {"/tmp": {"kind": "tmp"} },
950     "output_path": "/tmp",
951     "priority": 1,
952     "runtime_constraints": {}
953 }`, nil, 0, func(t *TestDockerClient) {
954                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
955                 t.logWriter.Close()
956         })
957
958         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
959         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
960         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
961 }
962
963 type ArvMountCmdLine struct {
964         Cmd   []string
965         token string
966 }
967
968 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
969         am.Cmd = c
970         am.token = token
971         return nil, nil
972 }
973
974 func stubCert(temp string) string {
975         path := temp + "/ca-certificates.crt"
976         crt, _ := os.Create(path)
977         crt.Close()
978         arvadosclient.CertFiles = []string{path}
979         return path
980 }
981
982 func (s *TestSuite) TestSetupMounts(c *C) {
983         api := &ArvTestClient{}
984         kc := &KeepTestClient{}
985         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
986         am := &ArvMountCmdLine{}
987         cr.RunArvMount = am.ArvMountTest
988
989         realTemp, err := ioutil.TempDir("", "crunchrun_test1-")
990         c.Assert(err, IsNil)
991         certTemp, err := ioutil.TempDir("", "crunchrun_test2-")
992         c.Assert(err, IsNil)
993         stubCertPath := stubCert(certTemp)
994
995         defer os.RemoveAll(realTemp)
996         defer os.RemoveAll(certTemp)
997
998         i := 0
999         cr.MkTempDir = func(_ string, prefix string) (string, error) {
1000                 i++
1001                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, i)
1002                 err := os.Mkdir(d, os.ModePerm)
1003                 if err != nil && strings.Contains(err.Error(), ": file exists") {
1004                         // Test case must have pre-populated the tempdir
1005                         err = nil
1006                 }
1007                 return d, err
1008         }
1009
1010         checkEmpty := func() {
1011                 filepath.Walk(realTemp, func(path string, _ os.FileInfo, err error) error {
1012                         c.Check(path, Equals, realTemp)
1013                         c.Check(err, IsNil)
1014                         return nil
1015                 })
1016         }
1017
1018         {
1019                 i = 0
1020                 cr.ArvMountPoint = ""
1021                 cr.Container.Mounts = make(map[string]arvados.Mount)
1022                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1023                 cr.OutputPath = "/tmp"
1024                 cr.statInterval = 5 * time.Second
1025                 err := cr.SetupMounts()
1026                 c.Check(err, IsNil)
1027                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1028                         "--read-write", "--crunchstat-interval=5",
1029                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1030                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp"})
1031                 os.RemoveAll(cr.ArvMountPoint)
1032                 cr.CleanupDirs()
1033                 checkEmpty()
1034         }
1035
1036         {
1037                 i = 0
1038                 cr.ArvMountPoint = ""
1039                 cr.Container.Mounts = make(map[string]arvados.Mount)
1040                 cr.Container.Mounts["/out"] = arvados.Mount{Kind: "tmp"}
1041                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1042                 cr.OutputPath = "/out"
1043
1044                 err := cr.SetupMounts()
1045                 c.Check(err, IsNil)
1046                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1047                         "--read-write", "--crunchstat-interval=5",
1048                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1049                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/out", realTemp + "/3:/tmp"})
1050                 os.RemoveAll(cr.ArvMountPoint)
1051                 cr.CleanupDirs()
1052                 checkEmpty()
1053         }
1054
1055         {
1056                 i = 0
1057                 cr.ArvMountPoint = ""
1058                 cr.Container.Mounts = make(map[string]arvados.Mount)
1059                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1060                 cr.OutputPath = "/tmp"
1061
1062                 apiflag := true
1063                 cr.Container.RuntimeConstraints.API = &apiflag
1064
1065                 err := cr.SetupMounts()
1066                 c.Check(err, IsNil)
1067                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1068                         "--read-write", "--crunchstat-interval=5",
1069                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1070                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", stubCertPath + ":/etc/arvados/ca-certificates.crt:ro"})
1071                 os.RemoveAll(cr.ArvMountPoint)
1072                 cr.CleanupDirs()
1073                 checkEmpty()
1074
1075                 apiflag = false
1076         }
1077
1078         {
1079                 i = 0
1080                 cr.ArvMountPoint = ""
1081                 cr.Container.Mounts = map[string]arvados.Mount{
1082                         "/keeptmp": {Kind: "collection", Writable: true},
1083                 }
1084                 cr.OutputPath = "/keeptmp"
1085
1086                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1087
1088                 err := cr.SetupMounts()
1089                 c.Check(err, IsNil)
1090                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1091                         "--read-write", "--crunchstat-interval=5",
1092                         "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1093                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
1094                 os.RemoveAll(cr.ArvMountPoint)
1095                 cr.CleanupDirs()
1096                 checkEmpty()
1097         }
1098
1099         {
1100                 i = 0
1101                 cr.ArvMountPoint = ""
1102                 cr.Container.Mounts = map[string]arvados.Mount{
1103                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1104                         "/keepout": {Kind: "collection", Writable: true},
1105                 }
1106                 cr.OutputPath = "/keepout"
1107
1108                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1109                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1110
1111                 err := cr.SetupMounts()
1112                 c.Check(err, IsNil)
1113                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1114                         "--read-write", "--crunchstat-interval=5",
1115                         "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1116                 sort.StringSlice(cr.Binds).Sort()
1117                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1118                         realTemp + "/keep1/tmp0:/keepout"})
1119                 os.RemoveAll(cr.ArvMountPoint)
1120                 cr.CleanupDirs()
1121                 checkEmpty()
1122         }
1123
1124         {
1125                 i = 0
1126                 cr.ArvMountPoint = ""
1127                 cr.Container.RuntimeConstraints.KeepCacheRAM = 512
1128                 cr.Container.Mounts = map[string]arvados.Mount{
1129                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1130                         "/keepout": {Kind: "collection", Writable: true},
1131                 }
1132                 cr.OutputPath = "/keepout"
1133
1134                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1135                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1136
1137                 err := cr.SetupMounts()
1138                 c.Check(err, IsNil)
1139                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1140                         "--read-write", "--crunchstat-interval=5",
1141                         "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1142                 sort.StringSlice(cr.Binds).Sort()
1143                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1144                         realTemp + "/keep1/tmp0:/keepout"})
1145                 os.RemoveAll(cr.ArvMountPoint)
1146                 cr.CleanupDirs()
1147                 checkEmpty()
1148         }
1149
1150         for _, test := range []struct {
1151                 in  interface{}
1152                 out string
1153         }{
1154                 {in: "foo", out: `"foo"`},
1155                 {in: nil, out: `null`},
1156                 {in: map[string]int64{"foo": 123456789123456789}, out: `{"foo":123456789123456789}`},
1157         } {
1158                 i = 0
1159                 cr.ArvMountPoint = ""
1160                 cr.Container.Mounts = map[string]arvados.Mount{
1161                         "/mnt/test.json": {Kind: "json", Content: test.in},
1162                 }
1163                 err := cr.SetupMounts()
1164                 c.Check(err, IsNil)
1165                 sort.StringSlice(cr.Binds).Sort()
1166                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
1167                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
1168                 c.Check(err, IsNil)
1169                 c.Check(content, DeepEquals, []byte(test.out))
1170                 os.RemoveAll(cr.ArvMountPoint)
1171                 cr.CleanupDirs()
1172                 checkEmpty()
1173         }
1174
1175         // Read-only mount points are allowed underneath output_dir mount point
1176         {
1177                 i = 0
1178                 cr.ArvMountPoint = ""
1179                 cr.Container.Mounts = make(map[string]arvados.Mount)
1180                 cr.Container.Mounts = map[string]arvados.Mount{
1181                         "/tmp":     {Kind: "tmp"},
1182                         "/tmp/foo": {Kind: "collection"},
1183                 }
1184                 cr.OutputPath = "/tmp"
1185
1186                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1187
1188                 err := cr.SetupMounts()
1189                 c.Check(err, IsNil)
1190                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1191                         "--read-write", "--crunchstat-interval=5",
1192                         "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1193                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", realTemp + "/keep1/tmp0:/tmp/foo:ro"})
1194                 os.RemoveAll(cr.ArvMountPoint)
1195                 cr.CleanupDirs()
1196                 checkEmpty()
1197         }
1198
1199         // Writable mount points are not allowed underneath output_dir mount point
1200         {
1201                 i = 0
1202                 cr.ArvMountPoint = ""
1203                 cr.Container.Mounts = make(map[string]arvados.Mount)
1204                 cr.Container.Mounts = map[string]arvados.Mount{
1205                         "/tmp":     {Kind: "tmp"},
1206                         "/tmp/foo": {Kind: "collection", Writable: true},
1207                 }
1208                 cr.OutputPath = "/tmp"
1209
1210                 err := cr.SetupMounts()
1211                 c.Check(err, NotNil)
1212                 c.Check(err, ErrorMatches, `Writable mount points are not permitted underneath the output_path.*`)
1213                 os.RemoveAll(cr.ArvMountPoint)
1214                 cr.CleanupDirs()
1215                 checkEmpty()
1216         }
1217
1218         // Only mount points of kind 'collection' are allowed underneath output_dir mount point
1219         {
1220                 i = 0
1221                 cr.ArvMountPoint = ""
1222                 cr.Container.Mounts = make(map[string]arvados.Mount)
1223                 cr.Container.Mounts = map[string]arvados.Mount{
1224                         "/tmp":     {Kind: "tmp"},
1225                         "/tmp/foo": {Kind: "json"},
1226                 }
1227                 cr.OutputPath = "/tmp"
1228
1229                 err := cr.SetupMounts()
1230                 c.Check(err, NotNil)
1231                 c.Check(err, ErrorMatches, `Only mount points of kind 'collection' are supported underneath the output_path.*`)
1232                 os.RemoveAll(cr.ArvMountPoint)
1233                 cr.CleanupDirs()
1234                 checkEmpty()
1235         }
1236
1237         // Only mount point of kind 'collection' is allowed for stdin
1238         {
1239                 i = 0
1240                 cr.ArvMountPoint = ""
1241                 cr.Container.Mounts = make(map[string]arvados.Mount)
1242                 cr.Container.Mounts = map[string]arvados.Mount{
1243                         "stdin": {Kind: "tmp"},
1244                 }
1245
1246                 err := cr.SetupMounts()
1247                 c.Check(err, NotNil)
1248                 c.Check(err, ErrorMatches, `Unsupported mount kind 'tmp' for stdin.*`)
1249                 os.RemoveAll(cr.ArvMountPoint)
1250                 cr.CleanupDirs()
1251                 checkEmpty()
1252         }
1253 }
1254
1255 func (s *TestSuite) TestStdout(c *C) {
1256         helperRecord := `{
1257                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1258                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1259                 "cwd": "/bin",
1260                 "environment": {"FROBIZ": "bilbo"},
1261                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
1262                 "output_path": "/tmp",
1263                 "priority": 1,
1264                 "runtime_constraints": {}
1265         }`
1266
1267         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1268                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1269                 t.logWriter.Close()
1270         })
1271
1272         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1273         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1274         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1275 }
1276
1277 // Used by the TestStdoutWithWrongPath*()
1278 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
1279         rec := arvados.Container{}
1280         err = json.Unmarshal([]byte(record), &rec)
1281         c.Check(err, IsNil)
1282
1283         docker := NewTestDockerClient(0)
1284         docker.fn = fn
1285         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
1286
1287         api = &ArvTestClient{Container: rec}
1288         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1289         am := &ArvMountCmdLine{}
1290         cr.RunArvMount = am.ArvMountTest
1291
1292         err = cr.Run()
1293         return
1294 }
1295
1296 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
1297         _, _, err := StdoutErrorRunHelper(c, `{
1298     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
1299     "output_path": "/tmp"
1300 }`, func(t *TestDockerClient) {})
1301
1302         c.Check(err, NotNil)
1303         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
1304 }
1305
1306 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
1307         _, _, err := StdoutErrorRunHelper(c, `{
1308     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
1309     "output_path": "/tmp"
1310 }`, func(t *TestDockerClient) {})
1311
1312         c.Check(err, NotNil)
1313         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
1314 }
1315
1316 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
1317         _, _, err := StdoutErrorRunHelper(c, `{
1318     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
1319     "output_path": "/tmp"
1320 }`, func(t *TestDockerClient) {})
1321
1322         c.Check(err, NotNil)
1323         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
1324 }
1325
1326 func (s *TestSuite) TestFullRunWithAPI(c *C) {
1327         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1328         defer os.Unsetenv("ARVADOS_API_HOST")
1329         api, _, _ := FullRunHelper(c, `{
1330     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1331     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1332     "cwd": "/bin",
1333     "environment": {},
1334     "mounts": {"/tmp": {"kind": "tmp"} },
1335     "output_path": "/tmp",
1336     "priority": 1,
1337     "runtime_constraints": {"API": true}
1338 }`, nil, 0, func(t *TestDockerClient) {
1339                 t.logWriter.Write(dockerLog(1, t.env[1][17:]+"\n"))
1340                 t.logWriter.Close()
1341         })
1342
1343         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1344         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1345         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "test.arvados.org\n"), Equals, true)
1346         c.Check(api.CalledWith("container.output", "d41d8cd98f00b204e9800998ecf8427e+0"), NotNil)
1347 }
1348
1349 func (s *TestSuite) TestFullRunSetOutput(c *C) {
1350         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1351         defer os.Unsetenv("ARVADOS_API_HOST")
1352         api, _, _ := FullRunHelper(c, `{
1353     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1354     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1355     "cwd": "/bin",
1356     "environment": {},
1357     "mounts": {"/tmp": {"kind": "tmp"} },
1358     "output_path": "/tmp",
1359     "priority": 1,
1360     "runtime_constraints": {"API": true}
1361 }`, nil, 0, func(t *TestDockerClient) {
1362                 t.api.Container.Output = "d4ab34d3d4f8a72f5c4973051ae69fab+122"
1363                 t.logWriter.Close()
1364         })
1365
1366         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1367         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1368         c.Check(api.CalledWith("container.output", "d4ab34d3d4f8a72f5c4973051ae69fab+122"), NotNil)
1369 }
1370
1371 func (s *TestSuite) TestStdoutWithExcludeFromOutputMountPointUnderOutputDir(c *C) {
1372         helperRecord := `{
1373                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1374                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1375                 "cwd": "/bin",
1376                 "environment": {"FROBIZ": "bilbo"},
1377                 "mounts": {
1378         "/tmp": {"kind": "tmp"},
1379         "/tmp/foo": {"kind": "collection",
1380                      "portable_data_hash": "a3e8f74c6f101eae01fa08bfb4e49b3a+54",
1381                      "exclude_from_output": true
1382         },
1383         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1384     },
1385                 "output_path": "/tmp",
1386                 "priority": 1,
1387                 "runtime_constraints": {}
1388         }`
1389
1390         extraMounts := []string{"a3e8f74c6f101eae01fa08bfb4e49b3a+54"}
1391
1392         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1393                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1394                 t.logWriter.Close()
1395         })
1396
1397         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1398         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1399         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1400 }
1401
1402 func (s *TestSuite) TestStdoutWithMultipleMountPointsUnderOutputDir(c *C) {
1403         helperRecord := `{
1404                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1405                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1406                 "cwd": "/bin",
1407                 "environment": {"FROBIZ": "bilbo"},
1408                 "mounts": {
1409         "/tmp": {"kind": "tmp"},
1410         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/file2_in_main.txt"},
1411         "/tmp/foo/sub1": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1"},
1412         "/tmp/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/file2_in_subdir1.txt"},
1413         "/tmp/foo/baz/sub2file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/subdir2/file2_in_subdir2.txt"},
1414         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1415     },
1416                 "output_path": "/tmp",
1417                 "priority": 1,
1418                 "runtime_constraints": {}
1419         }`
1420
1421         extraMounts := []string{
1422                 "a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt",
1423                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1424                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt",
1425         }
1426
1427         api, runner, realtemp := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1428                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1429                 t.logWriter.Close()
1430         })
1431
1432         c.Check(runner.Binds, DeepEquals, []string{realtemp + "/2:/tmp",
1433                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt:/tmp/foo/bar:ro",
1434                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt:/tmp/foo/baz/sub2file2:ro",
1435                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1:/tmp/foo/sub1:ro",
1436                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt:/tmp/foo/sub1file2:ro",
1437         })
1438
1439         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1440         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1441         for _, v := range api.Content {
1442                 if v["collection"] != nil {
1443                         c.Check(v["ensure_unique_name"], Equals, true)
1444                         collection := v["collection"].(arvadosclient.Dict)
1445                         if strings.Index(collection["name"].(string), "output") == 0 {
1446                                 manifest := collection["manifest_text"].(string)
1447
1448                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1449 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 9:18:bar 9:18:sub1file2
1450 ./foo/baz 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 9:18:sub2file2
1451 ./foo/sub1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
1452 ./foo/sub1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
1453 `)
1454                         }
1455                 }
1456         }
1457 }
1458
1459 func (s *TestSuite) TestStdoutWithMountPointsUnderOutputDirDenormalizedManifest(c *C) {
1460         helperRecord := `{
1461                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1462                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1463                 "cwd": "/bin",
1464                 "environment": {"FROBIZ": "bilbo"},
1465                 "mounts": {
1466         "/tmp": {"kind": "tmp"},
1467         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt"},
1468         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1469     },
1470                 "output_path": "/tmp",
1471                 "priority": 1,
1472                 "runtime_constraints": {}
1473         }`
1474
1475         extraMounts := []string{
1476                 "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1477         }
1478
1479         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1480                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1481                 t.logWriter.Close()
1482         })
1483
1484         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1485         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1486         for _, v := range api.Content {
1487                 if v["collection"] != nil {
1488                         collection := v["collection"].(arvadosclient.Dict)
1489                         if strings.Index(collection["name"].(string), "output") == 0 {
1490                                 manifest := collection["manifest_text"].(string)
1491
1492                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1493 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 10:17:bar
1494 `)
1495                         }
1496                 }
1497         }
1498 }
1499
1500 func (s *TestSuite) TestOutputSymlinkToInput(c *C) {
1501         helperRecord := `{
1502                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1503                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1504                 "cwd": "/bin",
1505                 "environment": {"FROBIZ": "bilbo"},
1506                 "mounts": {
1507         "/tmp": {"kind": "tmp"},
1508         "/keep/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path": "/subdir1/file2_in_subdir1.txt"},
1509         "/keep/foo2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367"}
1510     },
1511                 "output_path": "/tmp",
1512                 "priority": 1,
1513                 "runtime_constraints": {}
1514         }`
1515
1516         extraMounts := []string{
1517                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1518         }
1519
1520         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1521                 os.Symlink("/keep/foo/sub1file2", t.realTemp+"/2/baz")
1522                 os.Symlink("/keep/foo2/subdir1/file2_in_subdir1.txt", t.realTemp+"/2/baz2")
1523                 os.Symlink("/keep/foo2/subdir1", t.realTemp+"/2/baz3")
1524                 os.Mkdir(t.realTemp+"/2/baz4", 0700)
1525                 os.Symlink("/keep/foo2/subdir1/file2_in_subdir1.txt", t.realTemp+"/2/baz4/baz5")
1526                 t.logWriter.Close()
1527         })
1528
1529         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1530         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1531         for _, v := range api.Content {
1532                 if v["collection"] != nil {
1533                         collection := v["collection"].(arvadosclient.Dict)
1534                         if strings.Index(collection["name"].(string), "output") == 0 {
1535                                 manifest := collection["manifest_text"].(string)
1536                                 c.Check(manifest, Equals, `. 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 9:18:baz 9:18:baz2
1537 ./baz3 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
1538 ./baz3/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
1539 ./baz4 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 9:18:baz5
1540 `)
1541                         }
1542                 }
1543         }
1544 }
1545
1546 func (s *TestSuite) TestOutputError(c *C) {
1547         helperRecord := `{
1548                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1549                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1550                 "cwd": "/bin",
1551                 "environment": {"FROBIZ": "bilbo"},
1552                 "mounts": {
1553         "/tmp": {"kind": "tmp"}
1554     },
1555                 "output_path": "/tmp",
1556                 "priority": 1,
1557                 "runtime_constraints": {}
1558         }`
1559
1560         extraMounts := []string{}
1561
1562         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1563                 os.Symlink("/etc/hosts", t.realTemp+"/2/baz")
1564                 t.logWriter.Close()
1565         })
1566
1567         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
1568 }
1569
1570 func (s *TestSuite) TestOutputSymlinkToOutput(c *C) {
1571         helperRecord := `{
1572                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1573                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1574                 "cwd": "/bin",
1575                 "environment": {"FROBIZ": "bilbo"},
1576                 "mounts": {
1577         "/tmp": {"kind": "tmp"}
1578     },
1579                 "output_path": "/tmp",
1580                 "priority": 1,
1581                 "runtime_constraints": {}
1582         }`
1583
1584         extraMounts := []string{}
1585
1586         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1587                 rf, _ := os.Create(t.realTemp + "/2/realfile")
1588                 rf.Write([]byte("foo"))
1589                 rf.Close()
1590
1591                 os.Mkdir(t.realTemp+"/2/realdir", 0700)
1592                 rf, _ = os.Create(t.realTemp + "/2/realdir/subfile")
1593                 rf.Write([]byte("bar"))
1594                 rf.Close()
1595
1596                 os.Symlink("/tmp/realfile", t.realTemp+"/2/file1")
1597                 os.Symlink("realfile", t.realTemp+"/2/file2")
1598                 os.Symlink("/tmp/file1", t.realTemp+"/2/file3")
1599                 os.Symlink("file2", t.realTemp+"/2/file4")
1600                 os.Symlink("realdir", t.realTemp+"/2/dir1")
1601                 os.Symlink("/tmp/realdir", t.realTemp+"/2/dir2")
1602                 t.logWriter.Close()
1603         })
1604
1605         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1606         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1607         for _, v := range api.Content {
1608                 if v["collection"] != nil {
1609                         collection := v["collection"].(arvadosclient.Dict)
1610                         if strings.Index(collection["name"].(string), "output") == 0 {
1611                                 manifest := collection["manifest_text"].(string)
1612                                 c.Check(manifest, Equals,
1613                                         `. 7a2c86e102dcc231bd232aad99686dfa+15 0:3:file1 3:3:file2 6:3:file3 9:3:file4 12:3:realfile
1614 ./dir1 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1615 ./dir2 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1616 ./realdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1617 `)
1618                         }
1619                 }
1620         }
1621 }
1622
1623 func (s *TestSuite) TestStdinCollectionMountPoint(c *C) {
1624         helperRecord := `{
1625                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1626                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1627                 "cwd": "/bin",
1628                 "environment": {"FROBIZ": "bilbo"},
1629                 "mounts": {
1630         "/tmp": {"kind": "tmp"},
1631         "stdin": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367", "path": "/file1_in_main.txt"},
1632         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1633     },
1634                 "output_path": "/tmp",
1635                 "priority": 1,
1636                 "runtime_constraints": {}
1637         }`
1638
1639         extraMounts := []string{
1640                 "b0def87f80dd594d4675809e83bd4f15+367/file1_in_main.txt",
1641         }
1642
1643         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1644                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1645                 t.logWriter.Close()
1646         })
1647
1648         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1649         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1650         for _, v := range api.Content {
1651                 if v["collection"] != nil {
1652                         collection := v["collection"].(arvadosclient.Dict)
1653                         if strings.Index(collection["name"].(string), "output") == 0 {
1654                                 manifest := collection["manifest_text"].(string)
1655                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1656 `)
1657                         }
1658                 }
1659         }
1660 }
1661
1662 func (s *TestSuite) TestStdinJsonMountPoint(c *C) {
1663         helperRecord := `{
1664                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1665                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1666                 "cwd": "/bin",
1667                 "environment": {"FROBIZ": "bilbo"},
1668                 "mounts": {
1669         "/tmp": {"kind": "tmp"},
1670         "stdin": {"kind": "json", "content": "foo"},
1671         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1672     },
1673                 "output_path": "/tmp",
1674                 "priority": 1,
1675                 "runtime_constraints": {}
1676         }`
1677
1678         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1679                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1680                 t.logWriter.Close()
1681         })
1682
1683         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1684         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1685         for _, v := range api.Content {
1686                 if v["collection"] != nil {
1687                         collection := v["collection"].(arvadosclient.Dict)
1688                         if strings.Index(collection["name"].(string), "output") == 0 {
1689                                 manifest := collection["manifest_text"].(string)
1690                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1691 `)
1692                         }
1693                 }
1694         }
1695 }
1696
1697 func (s *TestSuite) TestStderrMount(c *C) {
1698         api, _, _ := FullRunHelper(c, `{
1699     "command": ["/bin/sh", "-c", "echo hello;exit 1"],
1700     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1701     "cwd": ".",
1702     "environment": {},
1703     "mounts": {"/tmp": {"kind": "tmp"},
1704                "stdout": {"kind": "file", "path": "/tmp/a/out.txt"},
1705                "stderr": {"kind": "file", "path": "/tmp/b/err.txt"}},
1706     "output_path": "/tmp",
1707     "priority": 1,
1708     "runtime_constraints": {}
1709 }`, nil, 1, func(t *TestDockerClient) {
1710                 t.logWriter.Write(dockerLog(1, "hello\n"))
1711                 t.logWriter.Write(dockerLog(2, "oops\n"))
1712                 t.logWriter.Close()
1713         })
1714
1715         final := api.CalledWith("container.state", "Complete")
1716         c.Assert(final, NotNil)
1717         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
1718         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
1719
1720         c.Check(api.CalledWith("collection.manifest_text", "./a b1946ac92492d2347c6235b4d2611184+6 0:6:out.txt\n./b 38af5c54926b620264ab1501150cf189+5 0:5:err.txt\n"), NotNil)
1721 }
1722
1723 func (s *TestSuite) TestNumberRoundTrip(c *C) {
1724         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1725         cr.fetchContainerRecord()
1726
1727         jsondata, err := json.Marshal(cr.Container.Mounts["/json"].Content)
1728
1729         c.Check(err, IsNil)
1730         c.Check(string(jsondata), Equals, `{"number":123456789123456789}`)
1731 }
1732
1733 func (s *TestSuite) TestEvalSymlinks(c *C) {
1734         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1735
1736         realTemp, err := ioutil.TempDir("", "crunchrun_test-")
1737         c.Assert(err, IsNil)
1738         defer os.RemoveAll(realTemp)
1739
1740         cr.HostOutputDir = realTemp
1741
1742         // Absolute path outside output dir
1743         os.Symlink("/etc/passwd", realTemp+"/p1")
1744
1745         // Relative outside output dir
1746         os.Symlink("../zip", realTemp+"/p2")
1747
1748         // Circular references
1749         os.Symlink("p4", realTemp+"/p3")
1750         os.Symlink("p5", realTemp+"/p4")
1751         os.Symlink("p3", realTemp+"/p5")
1752
1753         // Target doesn't exist
1754         os.Symlink("p99", realTemp+"/p6")
1755
1756         for _, v := range []string{"p1", "p2", "p3", "p4", "p5"} {
1757                 info, err := os.Lstat(realTemp + "/" + v)
1758                 _, _, _, err = cr.derefOutputSymlink(realTemp+"/"+v, info)
1759                 c.Assert(err, NotNil)
1760         }
1761 }
1762
1763 func (s *TestSuite) TestEvalSymlinkDir(c *C) {
1764         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1765
1766         realTemp, err := ioutil.TempDir("", "crunchrun_test-")
1767         c.Assert(err, IsNil)
1768         defer os.RemoveAll(realTemp)
1769
1770         cr.HostOutputDir = realTemp
1771
1772         // Absolute path outside output dir
1773         os.Symlink(".", realTemp+"/loop")
1774
1775         v := "loop"
1776         info, err := os.Lstat(realTemp + "/" + v)
1777         _, err = cr.UploadOutputFile(realTemp+"/"+v, info, err, []string{}, nil, "", "", 0)
1778         c.Assert(err, NotNil)
1779 }
1780
1781 func (s *TestSuite) TestFullBrokenDocker1(c *C) {
1782         tf, err := ioutil.TempFile("", "brokenNodeHook-")
1783         c.Assert(err, IsNil)
1784         defer os.Remove(tf.Name())
1785
1786         tf.Write([]byte(`#!/bin/sh
1787 exec echo killme
1788 `))
1789         tf.Close()
1790         os.Chmod(tf.Name(), 0700)
1791
1792         ech := tf.Name()
1793         brokenNodeHook = &ech
1794
1795         api, _, _ := FullRunHelper(c, `{
1796     "command": ["echo", "hello world"],
1797     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1798     "cwd": ".",
1799     "environment": {},
1800     "mounts": {"/tmp": {"kind": "tmp"} },
1801     "output_path": "/tmp",
1802     "priority": 1,
1803     "runtime_constraints": {}
1804 }`, nil, 2, func(t *TestDockerClient) {
1805                 t.logWriter.Write(dockerLog(1, "hello world\n"))
1806                 t.logWriter.Close()
1807         })
1808
1809         c.Check(api.CalledWith("container.state", "Queued"), NotNil)
1810         c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*unable to run containers.*")
1811         c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*Running broken node hook.*")
1812         c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*killme.*")
1813
1814 }
1815
1816 func (s *TestSuite) TestFullBrokenDocker2(c *C) {
1817         ech := ""
1818         brokenNodeHook = &ech
1819
1820         api, _, _ := FullRunHelper(c, `{
1821     "command": ["echo", "hello world"],
1822     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1823     "cwd": ".",
1824     "environment": {},
1825     "mounts": {"/tmp": {"kind": "tmp"} },
1826     "output_path": "/tmp",
1827     "priority": 1,
1828     "runtime_constraints": {}
1829 }`, nil, 2, func(t *TestDockerClient) {
1830                 t.logWriter.Write(dockerLog(1, "hello world\n"))
1831                 t.logWriter.Close()
1832         })
1833
1834         c.Check(api.CalledWith("container.state", "Queued"), NotNil)
1835         c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*unable to run containers.*")
1836         c.Check(api.Logs["crunch-run"].String(), Matches, "(?ms).*No broken node hook.*")
1837 }