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)
24 class MockDateTime(datetime.datetime):
27 return datetime.datetime(2018, 1, 1, 0, 0, 0, 0)
29 datetime.datetime = MockDateTime
31 class TestContainer(unittest.TestCase):
33 # The test passes no builder.resources
34 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
35 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
36 def test_run(self, keepdocker):
37 for enable_reuse in (True, False):
38 arv_docker_clear_cache()
40 runner = mock.MagicMock()
41 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
42 runner.ignore_docker_for_reuse = False
43 runner.intermediate_output_ttl = 0
44 runner.secret_store = cwltool.secrets.SecretStore()
46 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
47 runner.api.collections().get().execute.return_value = {
48 "portable_data_hash": "99999999999999999999999999999993+99"}
50 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
56 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
58 "class": "CommandLineTool"
60 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
61 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
62 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
63 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
64 metadata={"cwlVersion": "v1.0"})
65 arvtool.formatgraph = None
66 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_run_"+str(enable_reuse),
67 make_fs_access=make_fs_access, tmpdir="/tmp"):
68 j.run(enable_reuse=enable_reuse, priority=500)
69 runner.api.container_requests().create.assert_called_with(
70 body=JsonDiffMatcher({
72 'HOME': '/var/spool/cwl',
75 'name': 'test_run_'+str(enable_reuse),
76 'runtime_constraints': {
80 'use_existing': enable_reuse,
83 '/tmp': {'kind': 'tmp',
84 "capacity": 1073741824
86 '/var/spool/cwl': {'kind': 'tmp',
87 "capacity": 1073741824 }
90 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
91 'output_path': '/var/spool/cwl',
93 'container_image': 'arvados/jobs',
94 'command': ['ls', '/var/spool/cwl'],
95 'cwd': '/var/spool/cwl',
96 'scheduling_parameters': {},
101 # The test passes some fields in builder.resources
102 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
103 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
104 def test_resource_requirements(self, keepdocker):
105 arv_docker_clear_cache()
106 runner = mock.MagicMock()
107 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
108 runner.ignore_docker_for_reuse = False
109 runner.intermediate_output_ttl = 3600
110 runner.secret_store = cwltool.secrets.SecretStore()
112 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
114 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
115 runner.api.collections().get().execute.return_value = {
116 "portable_data_hash": "99999999999999999999999999999993+99"}
122 "class": "ResourceRequirement",
128 "class": "http://arvados.org/cwl#RuntimeConstraints",
131 "class": "http://arvados.org/cwl#APIRequirement",
133 "class": "http://arvados.org/cwl#PartitionRequirement",
136 "class": "http://arvados.org/cwl#IntermediateOutput",
139 "class": "http://arvados.org/cwl#ReuseRequirement",
144 "class": "CommandLineTool"
146 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
147 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
148 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers",
149 avsc_names=avsc_names, make_fs_access=make_fs_access,
150 loader=Loader({}), metadata={"cwlVersion": "v1.0"})
151 arvtool.formatgraph = None
152 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_resource_requirements",
153 make_fs_access=make_fs_access, tmpdir="/tmp"):
154 j.run(enable_reuse=True, priority=500)
156 call_args, call_kwargs = runner.api.container_requests().create.call_args
158 call_body_expected = {
160 'HOME': '/var/spool/cwl',
163 'name': 'test_resource_requirements',
164 'runtime_constraints': {
167 'keep_cache_ram': 536870912,
170 'use_existing': False,
173 '/tmp': {'kind': 'tmp',
174 "capacity": 4194304000 },
175 '/var/spool/cwl': {'kind': 'tmp',
176 "capacity": 5242880000 }
178 'state': 'Committed',
179 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
180 'output_path': '/var/spool/cwl',
182 'container_image': 'arvados/jobs',
184 'cwd': '/var/spool/cwl',
185 'scheduling_parameters': {
186 'partitions': ['blurb']
192 call_body = call_kwargs.get('body', None)
193 self.assertNotEqual(None, call_body)
194 for key in call_body:
195 self.assertEqual(call_body_expected.get(key), call_body.get(key))
198 # The test passes some fields in builder.resources
199 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
200 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
201 @mock.patch("arvados.collection.Collection")
202 def test_initial_work_dir(self, collection_mock, keepdocker):
203 arv_docker_clear_cache()
204 runner = mock.MagicMock()
205 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
206 runner.ignore_docker_for_reuse = False
207 runner.intermediate_output_ttl = 0
208 runner.secret_store = cwltool.secrets.SecretStore()
210 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
212 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
213 runner.api.collections().get().execute.return_value = {
214 "portable_data_hash": "99999999999999999999999999999993+99"}
216 sourcemock = mock.MagicMock()
217 def get_collection_mock(p):
219 return (sourcemock, p.split("/", 1)[1])
221 return (sourcemock, "")
222 runner.fs_access.get_collection.side_effect = get_collection_mock
224 vwdmock = mock.MagicMock()
225 collection_mock.return_value = vwdmock
226 vwdmock.portable_data_hash.return_value = "99999999999999999999999999999996+99"
232 "class": "InitialWorkDirRequirement",
236 "location": "keep:99999999999999999999999999999995+99/bar"
239 "class": "Directory",
241 "location": "keep:99999999999999999999999999999995+99"
245 "basename": "filename",
246 "location": "keep:99999999999999999999999999999995+99/baz/filename"
249 "class": "Directory",
250 "basename": "subdir",
251 "location": "keep:99999999999999999999999999999995+99/subdir"
256 "class": "CommandLineTool"
258 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
259 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
260 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers",
261 avsc_names=avsc_names, make_fs_access=make_fs_access,
262 loader=Loader({}), metadata={"cwlVersion": "v1.0"})
263 arvtool.formatgraph = None
264 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_initial_work_dir",
265 make_fs_access=make_fs_access, tmpdir="/tmp"):
268 call_args, call_kwargs = runner.api.container_requests().create.call_args
270 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
271 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
272 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
273 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
275 call_body_expected = {
277 'HOME': '/var/spool/cwl',
280 'name': 'test_initial_work_dir',
281 'runtime_constraints': {
285 'use_existing': True,
288 '/tmp': {'kind': 'tmp',
289 "capacity": 1073741824 },
290 '/var/spool/cwl': {'kind': 'tmp',
291 "capacity": 1073741824 },
292 '/var/spool/cwl/foo': {
293 'kind': 'collection',
295 'portable_data_hash': '99999999999999999999999999999996+99'
297 '/var/spool/cwl/foo2': {
298 'kind': 'collection',
300 'portable_data_hash': '99999999999999999999999999999996+99'
302 '/var/spool/cwl/filename': {
303 'kind': 'collection',
305 'portable_data_hash': '99999999999999999999999999999996+99'
307 '/var/spool/cwl/subdir': {
308 'kind': 'collection',
310 'portable_data_hash': '99999999999999999999999999999996+99'
313 'state': 'Committed',
314 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
315 'output_path': '/var/spool/cwl',
317 'container_image': 'arvados/jobs',
319 'cwd': '/var/spool/cwl',
320 'scheduling_parameters': {
326 call_body = call_kwargs.get('body', None)
327 self.assertNotEqual(None, call_body)
328 for key in call_body:
329 self.assertEqual(call_body_expected.get(key), call_body.get(key))
332 # Test redirecting stdin/stdout/stderr
333 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
334 def test_redirects(self, keepdocker):
335 arv_docker_clear_cache()
337 runner = mock.MagicMock()
338 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
339 runner.ignore_docker_for_reuse = False
340 runner.intermediate_output_ttl = 0
341 runner.secret_store = cwltool.secrets.SecretStore()
343 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
344 runner.api.collections().get().execute.return_value = {
345 "portable_data_hash": "99999999999999999999999999999993+99"}
347 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
353 "stdout": "stdout.txt",
354 "stderr": "stderr.txt",
355 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
356 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
358 "class": "CommandLineTool"
360 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
361 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
362 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
363 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
364 metadata={"cwlVersion": "v1.0"})
365 arvtool.formatgraph = None
366 for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_run_redirect",
367 make_fs_access=make_fs_access, tmpdir="/tmp"):
369 runner.api.container_requests().create.assert_called_with(
370 body=JsonDiffMatcher({
372 'HOME': '/var/spool/cwl',
375 'name': 'test_run_redirect',
376 'runtime_constraints': {
380 'use_existing': True,
383 '/tmp': {'kind': 'tmp',
384 "capacity": 1073741824 },
385 '/var/spool/cwl': {'kind': 'tmp',
386 "capacity": 1073741824 },
389 "path": "/var/spool/cwl/stderr.txt"
392 "kind": "collection",
394 "portable_data_hash": "99999999999999999999999999999996+99"
398 "path": "/var/spool/cwl/stdout.txt"
401 'state': 'Committed',
402 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
403 'output_path': '/var/spool/cwl',
405 'container_image': 'arvados/jobs',
406 'command': ['ls', '/var/spool/cwl'],
407 'cwd': '/var/spool/cwl',
408 'scheduling_parameters': {},
413 @mock.patch("arvados.collection.Collection")
414 def test_done(self, col):
415 api = mock.MagicMock()
417 runner = mock.MagicMock()
419 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
420 runner.num_retries = 0
421 runner.ignore_docker_for_reuse = False
422 runner.intermediate_output_ttl = 0
423 runner.secret_store = cwltool.secrets.SecretStore()
425 runner.api.containers().get().execute.return_value = {"state":"Complete",
429 col().open.return_value = []
431 arvjob = arvados_cwl.ArvadosContainer(runner)
432 arvjob.name = "testjob"
433 arvjob.builder = mock.MagicMock()
434 arvjob.output_callback = mock.MagicMock()
435 arvjob.collect_outputs = mock.MagicMock()
436 arvjob.successCodes = [0]
437 arvjob.outdir = "/var/spool/cwl"
438 arvjob.output_ttl = 3600
440 arvjob.collect_outputs.return_value = {"out": "stuff"}
444 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
445 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
446 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
447 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
448 "modified_at": "2017-05-26T12:01:22Z"
451 self.assertFalse(api.collections().create.called)
453 arvjob.collect_outputs.assert_called_with("keep:abc+123")
454 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
455 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
457 # The test passes no builder.resources
458 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
459 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
460 def test_mounts(self, keepdocker):
461 arv_docker_clear_cache()
463 runner = mock.MagicMock()
464 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
465 runner.ignore_docker_for_reuse = False
466 runner.intermediate_output_ttl = 0
467 runner.secret_store = cwltool.secrets.SecretStore()
469 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
470 runner.api.collections().get().execute.return_value = {
471 "portable_data_hash": "99999999999999999999999999999993+99"}
473 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
482 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
484 "class": "CommandLineTool"
486 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
487 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
488 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
489 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
490 metadata={"cwlVersion": "v1.0"})
491 arvtool.formatgraph = None
494 "class": "Directory",
495 "location": "keep:99999999999999999999999999999994+44",
499 "location": "keep:99999999999999999999999999999994+44/file1",
503 "location": "keep:99999999999999999999999999999994+44/file2",
508 for j in arvtool.job(job_order, mock.MagicMock(), basedir="", name="test_run_mounts",
509 make_fs_access=make_fs_access, tmpdir="/tmp"):
511 runner.api.container_requests().create.assert_called_with(
512 body=JsonDiffMatcher({
514 'HOME': '/var/spool/cwl',
517 'name': 'test_run_mounts',
518 'runtime_constraints': {
522 'use_existing': True,
525 "/keep/99999999999999999999999999999994+44": {
526 "kind": "collection",
527 "portable_data_hash": "99999999999999999999999999999994+44"
529 '/tmp': {'kind': 'tmp',
530 "capacity": 1073741824 },
531 '/var/spool/cwl': {'kind': 'tmp',
532 "capacity": 1073741824 }
534 'state': 'Committed',
535 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
536 'output_path': '/var/spool/cwl',
538 'container_image': 'arvados/jobs',
539 'command': ['ls', '/var/spool/cwl'],
540 'cwd': '/var/spool/cwl',
541 'scheduling_parameters': {},
546 # The test passes no builder.resources
547 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
548 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
549 def test_secrets(self, keepdocker):
550 arv_docker_clear_cache()
552 runner = mock.MagicMock()
553 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
554 runner.ignore_docker_for_reuse = False
555 runner.intermediate_output_ttl = 0
556 runner.secret_store = cwltool.secrets.SecretStore()
558 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
559 runner.api.collections().get().execute.return_value = {
560 "portable_data_hash": "99999999999999999999999999999993+99"}
562 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
564 tool = cmap({"arguments": ["md5sum", "example.conf"],
565 "class": "CommandLineTool",
568 "class": "http://commonwl.org/cwltool#Secrets",
574 "id": "#secret_job.cwl",
577 "id": "#secret_job.cwl/pw",
585 "class": "InitialWorkDirRequirement",
588 "entry": "username: user\npassword: $(inputs.pw)\n",
589 "entryname": "example.conf"
594 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
595 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
596 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names,
597 basedir="", make_fs_access=make_fs_access, loader=Loader({}),
598 metadata={"cwlVersion": "v1.0"})
599 arvtool.formatgraph = None
601 job_order = {"pw": "blorp"}
602 runner.secret_store.store(["pw"], job_order)
604 for j in arvtool.job(job_order, mock.MagicMock(), basedir="", name="test_secrets",
605 make_fs_access=make_fs_access, tmpdir="/tmp"):
606 j.run(enable_reuse=True, priority=500)
607 runner.api.container_requests().create.assert_called_with(
608 body=JsonDiffMatcher({
610 'HOME': '/var/spool/cwl',
613 'name': 'test_secrets',
614 'runtime_constraints': {
618 'use_existing': True,
621 '/tmp': {'kind': 'tmp',
622 "capacity": 1073741824
624 '/var/spool/cwl': {'kind': 'tmp',
625 "capacity": 1073741824 }
627 'state': 'Committed',
628 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
629 'output_path': '/var/spool/cwl',
631 'container_image': 'arvados/jobs',
632 'command': ['md5sum', 'example.conf'],
633 'cwd': '/var/spool/cwl',
634 'scheduling_parameters': {},
637 "/var/spool/cwl/example.conf": {
638 "content": "username: user\npassword: blorp\n",
644 def test_get_intermediate_collection_info(self):
645 arvrunner = mock.MagicMock()
646 arvrunner.intermediate_output_ttl = 60
647 arvrunner.api.containers().current().execute.return_value = {"uuid" : "zzzzz-8i9sb-zzzzzzzzzzzzzzz"}
649 container = arvados_cwl.ArvadosContainer(arvrunner)
651 info = container._get_intermediate_collection_info()
653 self.assertEqual(info["name"], "Intermediate collection")
654 self.assertEqual(info["trash_at"], datetime.datetime(2018, 1, 1, 0, 1))
655 self.assertEqual(info["properties"], {"type" : "Intermediate", "container" : "zzzzz-8i9sb-zzzzzzzzzzzzzzz"})