1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
6 import arvados_cwl.context
7 from arvados_cwl.arvdocker import arv_docker_clear_cache
14 import cwltool.process
15 import cwltool.secrets
16 from schema_salad.ref_resolver import Loader
17 from schema_salad.sourceline import cmap
19 from .matcher import JsonDiffMatcher
21 if not os.getenv('ARVADOS_DEBUG'):
22 logging.getLogger('arvados.cwl-runner').setLevel(logging.WARN)
23 logging.getLogger('arvados.arv-run').setLevel(logging.WARN)
25 class MockDateTime(datetime.datetime):
28 return datetime.datetime(2018, 1, 1, 0, 0, 0, 0)
30 datetime.datetime = MockDateTime
32 class TestContainer(unittest.TestCase):
34 def helper(self, runner, enable_reuse=True):
35 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
37 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
38 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
39 loadingContext = arvados_cwl.context.ArvLoadingContext(
40 {"avsc_names": avsc_names,
42 "make_fs_access": make_fs_access,
44 "metadata": {"cwlVersion": "v1.0"}})
45 runtimeContext = arvados_cwl.context.ArvRuntimeContext(
46 {"work_api": "containers",
48 "name": "test_run_"+str(enable_reuse),
49 "make_fs_access": make_fs_access,
51 "enable_reuse": enable_reuse,
54 return loadingContext, runtimeContext
56 # The test passes no builder.resources
57 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
58 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
59 def test_run(self, keepdocker):
60 for enable_reuse in (True, False):
61 arv_docker_clear_cache()
63 runner = mock.MagicMock()
64 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
65 runner.ignore_docker_for_reuse = False
66 runner.intermediate_output_ttl = 0
67 runner.secret_store = cwltool.secrets.SecretStore()
69 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
70 runner.api.collections().get().execute.return_value = {
71 "portable_data_hash": "99999999999999999999999999999993+99"}
77 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
79 "class": "CommandLineTool"
82 loadingContext, runtimeContext = self.helper(runner, enable_reuse)
84 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
85 arvtool.formatgraph = None
87 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
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 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
133 runner.api.collections().get().execute.return_value = {
134 "portable_data_hash": "99999999999999999999999999999993+99"}
140 "class": "ResourceRequirement",
146 "class": "http://arvados.org/cwl#RuntimeConstraints",
149 "class": "http://arvados.org/cwl#APIRequirement",
151 "class": "http://arvados.org/cwl#PartitionRequirement",
154 "class": "http://arvados.org/cwl#IntermediateOutput",
157 "class": "http://arvados.org/cwl#ReuseRequirement",
162 "class": "CommandLineTool"
165 loadingContext, runtimeContext = self.helper(runner)
166 runtimeContext.name = "test_resource_requirements"
168 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
169 arvtool.formatgraph = None
170 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
171 j.run(runtimeContext)
173 call_args, call_kwargs = runner.api.container_requests().create.call_args
175 call_body_expected = {
177 'HOME': '/var/spool/cwl',
180 'name': 'test_resource_requirements',
181 'runtime_constraints': {
184 'keep_cache_ram': 536870912,
187 'use_existing': False,
190 '/tmp': {'kind': 'tmp',
191 "capacity": 4194304000 },
192 '/var/spool/cwl': {'kind': 'tmp',
193 "capacity": 5242880000 }
195 'state': 'Committed',
196 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
197 'output_path': '/var/spool/cwl',
199 'container_image': 'arvados/jobs',
201 'cwd': '/var/spool/cwl',
202 'scheduling_parameters': {
203 'partitions': ['blurb']
209 call_body = call_kwargs.get('body', None)
210 self.assertNotEqual(None, call_body)
211 for key in call_body:
212 self.assertEqual(call_body_expected.get(key), call_body.get(key))
215 # The test passes some fields in builder.resources
216 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
217 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
218 @mock.patch("arvados.collection.Collection")
219 def test_initial_work_dir(self, collection_mock, keepdocker):
220 arv_docker_clear_cache()
221 runner = mock.MagicMock()
222 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
223 runner.ignore_docker_for_reuse = False
224 runner.intermediate_output_ttl = 0
225 runner.secret_store = cwltool.secrets.SecretStore()
227 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
228 runner.api.collections().get().execute.return_value = {
229 "portable_data_hash": "99999999999999999999999999999993+99"}
231 sourcemock = mock.MagicMock()
232 def get_collection_mock(p):
234 return (sourcemock, p.split("/", 1)[1])
236 return (sourcemock, "")
237 runner.fs_access.get_collection.side_effect = get_collection_mock
239 vwdmock = mock.MagicMock()
240 collection_mock.return_value = vwdmock
241 vwdmock.portable_data_hash.return_value = "99999999999999999999999999999996+99"
247 "class": "InitialWorkDirRequirement",
251 "location": "keep:99999999999999999999999999999995+99/bar"
254 "class": "Directory",
256 "location": "keep:99999999999999999999999999999995+99"
260 "basename": "filename",
261 "location": "keep:99999999999999999999999999999995+99/baz/filename"
264 "class": "Directory",
265 "basename": "subdir",
266 "location": "keep:99999999999999999999999999999995+99/subdir"
271 "class": "CommandLineTool"
274 loadingContext, runtimeContext = self.helper(runner)
275 runtimeContext.name = "test_initial_work_dir"
277 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
278 arvtool.formatgraph = None
279 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
280 j.run(runtimeContext)
282 call_args, call_kwargs = runner.api.container_requests().create.call_args
284 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
285 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
286 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
287 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
289 call_body_expected = {
291 'HOME': '/var/spool/cwl',
294 'name': 'test_initial_work_dir',
295 'runtime_constraints': {
299 'use_existing': True,
302 '/tmp': {'kind': 'tmp',
303 "capacity": 1073741824 },
304 '/var/spool/cwl': {'kind': 'tmp',
305 "capacity": 1073741824 },
306 '/var/spool/cwl/foo': {
307 'kind': 'collection',
309 'portable_data_hash': '99999999999999999999999999999996+99'
311 '/var/spool/cwl/foo2': {
312 'kind': 'collection',
314 'portable_data_hash': '99999999999999999999999999999996+99'
316 '/var/spool/cwl/filename': {
317 'kind': 'collection',
319 'portable_data_hash': '99999999999999999999999999999996+99'
321 '/var/spool/cwl/subdir': {
322 'kind': 'collection',
324 'portable_data_hash': '99999999999999999999999999999996+99'
327 'state': 'Committed',
328 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
329 'output_path': '/var/spool/cwl',
331 'container_image': 'arvados/jobs',
333 'cwd': '/var/spool/cwl',
334 'scheduling_parameters': {
340 call_body = call_kwargs.get('body', None)
341 self.assertNotEqual(None, call_body)
342 for key in call_body:
343 self.assertEqual(call_body_expected.get(key), call_body.get(key))
346 # Test redirecting stdin/stdout/stderr
347 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
348 def test_redirects(self, keepdocker):
349 arv_docker_clear_cache()
351 runner = mock.MagicMock()
352 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
353 runner.ignore_docker_for_reuse = False
354 runner.intermediate_output_ttl = 0
355 runner.secret_store = cwltool.secrets.SecretStore()
357 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
358 runner.api.collections().get().execute.return_value = {
359 "portable_data_hash": "99999999999999999999999999999993+99"}
361 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
367 "stdout": "stdout.txt",
368 "stderr": "stderr.txt",
369 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
370 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
372 "class": "CommandLineTool"
375 loadingContext, runtimeContext = self.helper(runner)
376 runtimeContext.name = "test_run_redirect"
378 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
379 arvtool.formatgraph = None
380 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
381 j.run(runtimeContext)
382 runner.api.container_requests().create.assert_called_with(
383 body=JsonDiffMatcher({
385 'HOME': '/var/spool/cwl',
388 'name': 'test_run_redirect',
389 'runtime_constraints': {
393 'use_existing': True,
396 '/tmp': {'kind': 'tmp',
397 "capacity": 1073741824 },
398 '/var/spool/cwl': {'kind': 'tmp',
399 "capacity": 1073741824 },
402 "path": "/var/spool/cwl/stderr.txt"
405 "kind": "collection",
407 "portable_data_hash": "99999999999999999999999999999996+99"
411 "path": "/var/spool/cwl/stdout.txt"
414 'state': 'Committed',
415 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
416 'output_path': '/var/spool/cwl',
418 'container_image': 'arvados/jobs',
419 'command': ['ls', '/var/spool/cwl'],
420 'cwd': '/var/spool/cwl',
421 'scheduling_parameters': {},
426 @mock.patch("arvados.collection.Collection")
427 def test_done(self, col):
428 api = mock.MagicMock()
430 runner = mock.MagicMock()
432 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
433 runner.num_retries = 0
434 runner.ignore_docker_for_reuse = False
435 runner.intermediate_output_ttl = 0
436 runner.secret_store = cwltool.secrets.SecretStore()
438 runner.api.containers().get().execute.return_value = {"state":"Complete",
442 col().open.return_value = []
444 arvjob = arvados_cwl.ArvadosContainer(runner,
451 arvjob.output_callback = mock.MagicMock()
452 arvjob.collect_outputs = mock.MagicMock()
453 arvjob.successCodes = [0]
454 arvjob.outdir = "/var/spool/cwl"
455 arvjob.output_ttl = 3600
457 arvjob.collect_outputs.return_value = {"out": "stuff"}
461 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
462 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
463 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
464 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
465 "modified_at": "2017-05-26T12:01:22Z"
468 self.assertFalse(api.collections().create.called)
470 arvjob.collect_outputs.assert_called_with("keep:abc+123")
471 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
472 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
474 # The test passes no builder.resources
475 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
476 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
477 def test_mounts(self, keepdocker):
478 arv_docker_clear_cache()
480 runner = mock.MagicMock()
481 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
482 runner.ignore_docker_for_reuse = False
483 runner.intermediate_output_ttl = 0
484 runner.secret_store = cwltool.secrets.SecretStore()
486 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
487 runner.api.collections().get().execute.return_value = {
488 "portable_data_hash": "99999999999999999999999999999993+99"}
490 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
499 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
501 "class": "CommandLineTool"
504 loadingContext, runtimeContext = self.helper(runner)
505 runtimeContext.name = "test_run_mounts"
507 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
508 arvtool.formatgraph = None
511 "class": "Directory",
512 "location": "keep:99999999999999999999999999999994+44",
516 "location": "keep:99999999999999999999999999999994+44/file1",
520 "location": "keep:99999999999999999999999999999994+44/file2",
525 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
526 j.run(runtimeContext)
527 runner.api.container_requests().create.assert_called_with(
528 body=JsonDiffMatcher({
530 'HOME': '/var/spool/cwl',
533 'name': 'test_run_mounts',
534 'runtime_constraints': {
538 'use_existing': True,
541 "/keep/99999999999999999999999999999994+44": {
542 "kind": "collection",
543 "portable_data_hash": "99999999999999999999999999999994+44"
545 '/tmp': {'kind': 'tmp',
546 "capacity": 1073741824 },
547 '/var/spool/cwl': {'kind': 'tmp',
548 "capacity": 1073741824 }
550 'state': 'Committed',
551 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
552 'output_path': '/var/spool/cwl',
554 'container_image': 'arvados/jobs',
555 'command': ['ls', '/var/spool/cwl'],
556 'cwd': '/var/spool/cwl',
557 'scheduling_parameters': {},
562 # The test passes no builder.resources
563 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
564 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
565 def test_secrets(self, keepdocker):
566 arv_docker_clear_cache()
568 runner = mock.MagicMock()
569 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
570 runner.ignore_docker_for_reuse = False
571 runner.intermediate_output_ttl = 0
572 runner.secret_store = cwltool.secrets.SecretStore()
574 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
575 runner.api.collections().get().execute.return_value = {
576 "portable_data_hash": "99999999999999999999999999999993+99"}
578 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
580 tool = cmap({"arguments": ["md5sum", "example.conf"],
581 "class": "CommandLineTool",
584 "class": "http://commonwl.org/cwltool#Secrets",
590 "id": "#secret_job.cwl",
593 "id": "#secret_job.cwl/pw",
601 "class": "InitialWorkDirRequirement",
604 "entry": "username: user\npassword: $(inputs.pw)\n",
605 "entryname": "example.conf"
611 loadingContext, runtimeContext = self.helper(runner)
612 runtimeContext.name = "test_secrets"
614 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
615 arvtool.formatgraph = None
617 job_order = {"pw": "blorp"}
618 runner.secret_store.store(["pw"], job_order)
620 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
621 j.run(runtimeContext)
622 runner.api.container_requests().create.assert_called_with(
623 body=JsonDiffMatcher({
625 'HOME': '/var/spool/cwl',
628 'name': 'test_secrets',
629 'runtime_constraints': {
633 'use_existing': True,
636 '/tmp': {'kind': 'tmp',
637 "capacity": 1073741824
639 '/var/spool/cwl': {'kind': 'tmp',
640 "capacity": 1073741824 }
642 'state': 'Committed',
643 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
644 'output_path': '/var/spool/cwl',
646 'container_image': 'arvados/jobs',
647 'command': ['md5sum', 'example.conf'],
648 'cwd': '/var/spool/cwl',
649 'scheduling_parameters': {},
652 "/var/spool/cwl/example.conf": {
653 "content": "username: user\npassword: blorp\n",
659 # The test passes no builder.resources
660 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
661 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
662 def test_timelimit(self, keepdocker):
663 arv_docker_clear_cache()
665 runner = mock.MagicMock()
666 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
667 runner.ignore_docker_for_reuse = False
668 runner.intermediate_output_ttl = 0
669 runner.secret_store = cwltool.secrets.SecretStore()
671 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
672 runner.api.collections().get().execute.return_value = {
673 "portable_data_hash": "99999999999999999999999999999993+99"}
679 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
681 "class": "CommandLineTool",
684 "class": "http://commonwl.org/cwltool#TimeLimit",
690 loadingContext, runtimeContext = self.helper(runner)
691 runtimeContext.name = "test_timelimit"
693 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
694 arvtool.formatgraph = None
696 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
697 j.run(runtimeContext)
699 _, kwargs = runner.api.container_requests().create.call_args
700 self.assertEqual(42, kwargs['body']['scheduling_parameters'].get('max_run_time'))
703 def test_get_intermediate_collection_info(self):
704 arvrunner = mock.MagicMock()
705 arvrunner.intermediate_output_ttl = 60
706 arvrunner.api.containers().current().execute.return_value = {"uuid" : "zzzzz-8i9sb-zzzzzzzzzzzzzzz"}
708 container = arvados_cwl.ArvadosContainer(arvrunner)
710 info = container._get_intermediate_collection_info()
712 self.assertEqual(info["name"], "Intermediate collection")
713 self.assertEqual(info["trash_at"], datetime.datetime(2018, 1, 1, 0, 1))
714 self.assertEqual(info["properties"], {"type" : "Intermediate", "container" : "zzzzz-8i9sb-zzzzzzzzzzzzzzz"})