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