1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
6 from arvados_cwl.arvdocker import arv_docker_clear_cache
13 import cwltool.process
14 import cwltool.secrets
15 from schema_salad.ref_resolver import Loader
16 from schema_salad.sourceline import cmap
18 from .matcher import JsonDiffMatcher
20 if not os.getenv('ARVADOS_DEBUG'):
21 logging.getLogger('arvados.cwl-runner').setLevel(logging.WARN)
22 logging.getLogger('arvados.arv-run').setLevel(logging.WARN)
25 class CollectionMock(object):
26 def __init__(self, vwdmock, *args, **kwargs):
27 self.vwdmock = vwdmock
30 def open(self, *args, **kwargs):
32 return self.vwdmock.open(*args, **kwargs)
34 def copy(self, *args, **kwargs):
36 self.vwdmock.copy(*args, **kwargs)
38 def save_new(self, *args, **kwargs):
44 def portable_data_hash(self):
46 return arvados.config.EMPTY_BLOCK_LOCATOR
48 return "99999999999999999999999999999996+99"
51 class TestContainer(unittest.TestCase):
53 # The test passes no builder.resources
54 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
55 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
56 def test_run(self, keepdocker):
57 for enable_reuse in (True, False):
58 arv_docker_clear_cache()
60 runner = mock.MagicMock()
61 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
62 runner.ignore_docker_for_reuse = False
63 runner.intermediate_output_ttl = 0
64 runner.secret_store = cwltool.secrets.SecretStore()
66 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
67 runner.api.collections().get().execute.return_value = {
68 "portable_data_hash": "99999999999999999999999999999993+99"}
70 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
76 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
78 "class": "CommandLineTool"
80 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
81 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
82 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
83 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
84 metadata={"cwlVersion": "v1.0"})
85 arvtool.formatgraph = None
86 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_run_"+str(enable_reuse),
87 make_fs_access=make_fs_access, tmpdir="/tmp"):
88 j.run(enable_reuse=enable_reuse, priority=500)
89 runner.api.container_requests().create.assert_called_with(
90 body=JsonDiffMatcher({
92 'HOME': '/var/spool/cwl',
95 'name': 'test_run_'+str(enable_reuse),
96 'runtime_constraints': {
100 'use_existing': enable_reuse,
103 '/tmp': {'kind': 'tmp',
104 "capacity": 1073741824
106 '/var/spool/cwl': {'kind': 'tmp',
107 "capacity": 1073741824 }
109 'state': 'Committed',
110 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
111 'output_path': '/var/spool/cwl',
113 'container_image': 'arvados/jobs',
114 'command': ['ls', '/var/spool/cwl'],
115 'cwd': '/var/spool/cwl',
116 'scheduling_parameters': {},
121 # The test passes some fields in builder.resources
122 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
123 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
124 def test_resource_requirements(self, keepdocker):
125 arv_docker_clear_cache()
126 runner = mock.MagicMock()
127 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
128 runner.ignore_docker_for_reuse = False
129 runner.intermediate_output_ttl = 3600
130 runner.secret_store = cwltool.secrets.SecretStore()
132 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
134 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
135 runner.api.collections().get().execute.return_value = {
136 "portable_data_hash": "99999999999999999999999999999993+99"}
142 "class": "ResourceRequirement",
148 "class": "http://arvados.org/cwl#RuntimeConstraints",
151 "class": "http://arvados.org/cwl#APIRequirement",
153 "class": "http://arvados.org/cwl#PartitionRequirement",
156 "class": "http://arvados.org/cwl#IntermediateOutput",
159 "class": "http://arvados.org/cwl#ReuseRequirement",
164 "class": "CommandLineTool"
166 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
167 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
168 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers",
169 avsc_names=avsc_names, make_fs_access=make_fs_access,
170 loader=Loader({}), metadata={"cwlVersion": "v1.0"})
171 arvtool.formatgraph = None
172 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_resource_requirements",
173 make_fs_access=make_fs_access, tmpdir="/tmp"):
174 j.run(enable_reuse=True, priority=500)
176 call_args, call_kwargs = runner.api.container_requests().create.call_args
178 call_body_expected = {
180 'HOME': '/var/spool/cwl',
183 'name': 'test_resource_requirements',
184 'runtime_constraints': {
187 'keep_cache_ram': 536870912,
190 'use_existing': False,
193 '/tmp': {'kind': 'tmp',
194 "capacity": 4194304000 },
195 '/var/spool/cwl': {'kind': 'tmp',
196 "capacity": 5242880000 }
198 'state': 'Committed',
199 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
200 'output_path': '/var/spool/cwl',
202 'container_image': 'arvados/jobs',
204 'cwd': '/var/spool/cwl',
205 'scheduling_parameters': {
206 'partitions': ['blurb']
212 call_body = call_kwargs.get('body', None)
213 self.assertNotEqual(None, call_body)
214 for key in call_body:
215 self.assertEqual(call_body_expected.get(key), call_body.get(key))
218 # The test passes some fields in builder.resources
219 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
220 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
221 @mock.patch("arvados.collection.Collection")
222 def test_initial_work_dir(self, collection_mock, keepdocker):
223 arv_docker_clear_cache()
224 runner = mock.MagicMock()
225 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
226 runner.ignore_docker_for_reuse = False
227 runner.intermediate_output_ttl = 0
228 runner.secret_store = cwltool.secrets.SecretStore()
230 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
232 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
233 runner.api.collections().get().execute.return_value = {
234 "portable_data_hash": "99999999999999999999999999999993+99"}
236 sourcemock = mock.MagicMock()
237 def get_collection_mock(p):
239 return (sourcemock, p.split("/", 1)[1])
241 return (sourcemock, "")
242 runner.fs_access.get_collection.side_effect = get_collection_mock
244 vwdmock = mock.MagicMock()
245 collection_mock.side_effect = lambda *args, **kwargs: CollectionMock(vwdmock, *args, **kwargs)
251 "class": "InitialWorkDirRequirement",
255 "location": "keep:99999999999999999999999999999995+99/bar"
258 "class": "Directory",
260 "location": "keep:99999999999999999999999999999995+99"
264 "basename": "filename",
265 "location": "keep:99999999999999999999999999999995+99/baz/filename"
268 "class": "Directory",
269 "basename": "subdir",
270 "location": "keep:99999999999999999999999999999995+99/subdir"
275 "class": "CommandLineTool"
277 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
278 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
279 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers",
280 avsc_names=avsc_names, make_fs_access=make_fs_access,
281 loader=Loader({}), metadata={"cwlVersion": "v1.0"})
282 arvtool.formatgraph = None
283 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_initial_work_dir",
284 make_fs_access=make_fs_access, tmpdir="/tmp"):
287 call_args, call_kwargs = runner.api.container_requests().create.call_args
289 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
290 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
291 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
292 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
294 call_body_expected = {
296 'HOME': '/var/spool/cwl',
299 'name': 'test_initial_work_dir',
300 'runtime_constraints': {
304 'use_existing': True,
307 '/tmp': {'kind': 'tmp',
308 "capacity": 1073741824 },
309 '/var/spool/cwl': {'kind': 'tmp',
310 "capacity": 1073741824 },
311 '/var/spool/cwl/foo': {
312 'kind': 'collection',
314 'portable_data_hash': '99999999999999999999999999999996+99'
316 '/var/spool/cwl/foo2': {
317 'kind': 'collection',
319 'portable_data_hash': '99999999999999999999999999999996+99'
321 '/var/spool/cwl/filename': {
322 'kind': 'collection',
324 'portable_data_hash': '99999999999999999999999999999996+99'
326 '/var/spool/cwl/subdir': {
327 'kind': 'collection',
329 'portable_data_hash': '99999999999999999999999999999996+99'
332 'state': 'Committed',
333 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
334 'output_path': '/var/spool/cwl',
336 'container_image': 'arvados/jobs',
338 'cwd': '/var/spool/cwl',
339 'scheduling_parameters': {
345 call_body = call_kwargs.get('body', None)
346 self.assertNotEqual(None, call_body)
347 for key in call_body:
348 self.assertEqual(call_body_expected.get(key), call_body.get(key))
351 # Test redirecting stdin/stdout/stderr
352 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
353 def test_redirects(self, keepdocker):
354 arv_docker_clear_cache()
356 runner = mock.MagicMock()
357 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
358 runner.ignore_docker_for_reuse = False
359 runner.intermediate_output_ttl = 0
360 runner.secret_store = cwltool.secrets.SecretStore()
362 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
363 runner.api.collections().get().execute.return_value = {
364 "portable_data_hash": "99999999999999999999999999999993+99"}
366 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
372 "stdout": "stdout.txt",
373 "stderr": "stderr.txt",
374 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
375 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
377 "class": "CommandLineTool"
379 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
380 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
381 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
382 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
383 metadata={"cwlVersion": "v1.0"})
384 arvtool.formatgraph = None
385 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_run_redirect",
386 make_fs_access=make_fs_access, tmpdir="/tmp"):
388 runner.api.container_requests().create.assert_called_with(
389 body=JsonDiffMatcher({
391 'HOME': '/var/spool/cwl',
394 'name': 'test_run_redirect',
395 'runtime_constraints': {
399 'use_existing': True,
402 '/tmp': {'kind': 'tmp',
403 "capacity": 1073741824 },
404 '/var/spool/cwl': {'kind': 'tmp',
405 "capacity": 1073741824 },
408 "path": "/var/spool/cwl/stderr.txt"
411 "kind": "collection",
413 "portable_data_hash": "99999999999999999999999999999996+99"
417 "path": "/var/spool/cwl/stdout.txt"
420 'state': 'Committed',
421 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
422 'output_path': '/var/spool/cwl',
424 'container_image': 'arvados/jobs',
425 'command': ['ls', '/var/spool/cwl'],
426 'cwd': '/var/spool/cwl',
427 'scheduling_parameters': {},
432 @mock.patch("arvados.collection.Collection")
433 def test_done(self, col):
434 api = mock.MagicMock()
436 runner = mock.MagicMock()
438 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
439 runner.num_retries = 0
440 runner.ignore_docker_for_reuse = False
441 runner.intermediate_output_ttl = 0
442 runner.secret_store = cwltool.secrets.SecretStore()
444 runner.api.containers().get().execute.return_value = {"state":"Complete",
448 col().open.return_value = []
450 arvjob = arvados_cwl.ArvadosContainer(runner)
451 arvjob.name = "testjob"
452 arvjob.builder = mock.MagicMock()
453 arvjob.output_callback = mock.MagicMock()
454 arvjob.collect_outputs = mock.MagicMock()
455 arvjob.successCodes = [0]
456 arvjob.outdir = "/var/spool/cwl"
457 arvjob.output_ttl = 3600
459 arvjob.collect_outputs.return_value = {"out": "stuff"}
463 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
464 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
465 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
466 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
467 "modified_at": "2017-05-26T12:01:22Z"
470 self.assertFalse(api.collections().create.called)
472 arvjob.collect_outputs.assert_called_with("keep:abc+123")
473 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
474 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
476 # The test passes no builder.resources
477 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
478 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
479 def test_mounts(self, keepdocker):
480 arv_docker_clear_cache()
482 runner = mock.MagicMock()
483 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
484 runner.ignore_docker_for_reuse = False
485 runner.intermediate_output_ttl = 0
486 runner.secret_store = cwltool.secrets.SecretStore()
488 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
489 runner.api.collections().get().execute.return_value = {
490 "portable_data_hash": "99999999999999999999999999999993+99"}
492 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
501 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
503 "class": "CommandLineTool"
505 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
506 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
507 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
508 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
509 metadata={"cwlVersion": "v1.0"})
510 arvtool.formatgraph = None
513 "class": "Directory",
514 "location": "keep:99999999999999999999999999999994+44",
518 "location": "keep:99999999999999999999999999999994+44/file1",
522 "location": "keep:99999999999999999999999999999994+44/file2",
527 for j in arvtool.job(job_order, mock.MagicMock(), basedir="", name="test_run_mounts",
528 make_fs_access=make_fs_access, tmpdir="/tmp"):
530 runner.api.container_requests().create.assert_called_with(
531 body=JsonDiffMatcher({
533 'HOME': '/var/spool/cwl',
536 'name': 'test_run_mounts',
537 'runtime_constraints': {
541 'use_existing': True,
544 "/keep/99999999999999999999999999999994+44": {
545 "kind": "collection",
546 "portable_data_hash": "99999999999999999999999999999994+44"
548 '/tmp': {'kind': 'tmp',
549 "capacity": 1073741824 },
550 '/var/spool/cwl': {'kind': 'tmp',
551 "capacity": 1073741824 }
553 'state': 'Committed',
554 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
555 'output_path': '/var/spool/cwl',
557 'container_image': 'arvados/jobs',
558 'command': ['ls', '/var/spool/cwl'],
559 'cwd': '/var/spool/cwl',
560 'scheduling_parameters': {},
565 # The test passes no builder.resources
566 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
567 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
568 def test_secrets(self, keepdocker):
569 arv_docker_clear_cache()
571 runner = mock.MagicMock()
572 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
573 runner.ignore_docker_for_reuse = False
574 runner.intermediate_output_ttl = 0
575 runner.secret_store = cwltool.secrets.SecretStore()
577 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
578 runner.api.collections().get().execute.return_value = {
579 "portable_data_hash": "99999999999999999999999999999993+99"}
581 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
583 tool = cmap({"arguments": ["md5sum", "example.conf"],
584 "class": "CommandLineTool",
587 "class": "http://commonwl.org/cwltool#Secrets",
593 "id": "#secret_job.cwl",
596 "id": "#secret_job.cwl/pw",
604 "class": "InitialWorkDirRequirement",
607 "entry": "username: user\npassword: $(inputs.pw)\n",
608 "entryname": "example.conf"
613 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
614 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
615 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
616 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
617 metadata={"cwlVersion": "v1.0"})
618 arvtool.formatgraph = None
620 job_order = {"pw": "blorp"}
621 runner.secret_store.store(["pw"], job_order)
623 for j in arvtool.job(job_order, mock.MagicMock(), basedir="", name="test_secrets",
624 make_fs_access=make_fs_access, tmpdir="/tmp"):
625 j.run(enable_reuse=True, priority=500)
626 runner.api.container_requests().create.assert_called_with(
627 body=JsonDiffMatcher({
629 'HOME': '/var/spool/cwl',
632 'name': 'test_secrets',
633 'runtime_constraints': {
637 'use_existing': True,
640 '/tmp': {'kind': 'tmp',
641 "capacity": 1073741824
643 '/var/spool/cwl': {'kind': 'tmp',
644 "capacity": 1073741824 }
646 'state': 'Committed',
647 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
648 'output_path': '/var/spool/cwl',
650 'container_image': 'arvados/jobs',
651 'command': ['md5sum', 'example.conf'],
652 'cwd': '/var/spool/cwl',
653 'scheduling_parameters': {},
656 "/var/spool/cwl/example.conf": {
657 "content": "username: user\npassword: blorp\n",