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
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 TestContainer(unittest.TestCase):
26 def helper(self, runner, enable_reuse=True):
27 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
29 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
30 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
31 loadingContext = arvados_cwl.context.ArvLoadingContext(
32 {"avsc_names": avsc_names,
34 "make_fs_access": make_fs_access,
36 "metadata": {"cwlVersion": "v1.0"}})
37 runtimeContext = arvados_cwl.context.ArvRuntimeContext(
38 {"work_api": "containers",
40 "name": "test_run_"+str(enable_reuse),
41 "make_fs_access": make_fs_access,
43 "enable_reuse": enable_reuse,
46 return loadingContext, runtimeContext
48 # The test passes no builder.resources
49 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
50 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
51 def test_run(self, keepdocker):
52 for enable_reuse in (True, False):
53 arv_docker_clear_cache()
55 runner = mock.MagicMock()
56 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
57 runner.ignore_docker_for_reuse = False
58 runner.intermediate_output_ttl = 0
59 runner.secret_store = cwltool.secrets.SecretStore()
61 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
62 runner.api.collections().get().execute.return_value = {
63 "portable_data_hash": "99999999999999999999999999999993+99"}
69 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
71 "class": "CommandLineTool"
74 loadingContext, runtimeContext = self.helper(runner, enable_reuse)
76 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
77 arvtool.formatgraph = None
79 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
81 runner.api.container_requests().create.assert_called_with(
82 body=JsonDiffMatcher({
84 'HOME': '/var/spool/cwl',
87 'name': 'test_run_'+str(enable_reuse),
88 'runtime_constraints': {
92 'use_existing': enable_reuse,
95 '/tmp': {'kind': 'tmp',
96 "capacity": 1073741824
98 '/var/spool/cwl': {'kind': 'tmp',
99 "capacity": 1073741824 }
101 'state': 'Committed',
102 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
103 'output_path': '/var/spool/cwl',
105 'container_image': 'arvados/jobs',
106 'command': ['ls', '/var/spool/cwl'],
107 'cwd': '/var/spool/cwl',
108 'scheduling_parameters': {},
113 # The test passes some fields in builder.resources
114 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
115 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
116 def test_resource_requirements(self, keepdocker):
117 arv_docker_clear_cache()
118 runner = mock.MagicMock()
119 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
120 runner.ignore_docker_for_reuse = False
121 runner.intermediate_output_ttl = 3600
122 runner.secret_store = cwltool.secrets.SecretStore()
124 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
125 runner.api.collections().get().execute.return_value = {
126 "portable_data_hash": "99999999999999999999999999999993+99"}
132 "class": "ResourceRequirement",
138 "class": "http://arvados.org/cwl#RuntimeConstraints",
141 "class": "http://arvados.org/cwl#APIRequirement",
143 "class": "http://arvados.org/cwl#PartitionRequirement",
146 "class": "http://arvados.org/cwl#IntermediateOutput",
149 "class": "http://arvados.org/cwl#ReuseRequirement",
154 "class": "CommandLineTool"
157 loadingContext, runtimeContext = self.helper(runner)
158 runtimeContext.name = "test_resource_requirements"
160 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
161 arvtool.formatgraph = None
162 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
163 j.run(runtimeContext)
165 call_args, call_kwargs = runner.api.container_requests().create.call_args
167 call_body_expected = {
169 'HOME': '/var/spool/cwl',
172 'name': 'test_resource_requirements',
173 'runtime_constraints': {
176 'keep_cache_ram': 536870912,
179 'use_existing': False,
182 '/tmp': {'kind': 'tmp',
183 "capacity": 4194304000 },
184 '/var/spool/cwl': {'kind': 'tmp',
185 "capacity": 5242880000 }
187 'state': 'Committed',
188 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
189 'output_path': '/var/spool/cwl',
191 'container_image': 'arvados/jobs',
193 'cwd': '/var/spool/cwl',
194 'scheduling_parameters': {
195 'partitions': ['blurb']
201 call_body = call_kwargs.get('body', None)
202 self.assertNotEqual(None, call_body)
203 for key in call_body:
204 self.assertEqual(call_body_expected.get(key), call_body.get(key))
207 # The test passes some fields in builder.resources
208 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
209 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
210 @mock.patch("arvados.collection.Collection")
211 def test_initial_work_dir(self, collection_mock, keepdocker):
212 arv_docker_clear_cache()
213 runner = mock.MagicMock()
214 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
215 runner.ignore_docker_for_reuse = False
216 runner.intermediate_output_ttl = 0
217 runner.secret_store = cwltool.secrets.SecretStore()
219 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
220 runner.api.collections().get().execute.return_value = {
221 "portable_data_hash": "99999999999999999999999999999993+99"}
223 sourcemock = mock.MagicMock()
224 def get_collection_mock(p):
226 return (sourcemock, p.split("/", 1)[1])
228 return (sourcemock, "")
229 runner.fs_access.get_collection.side_effect = get_collection_mock
231 vwdmock = mock.MagicMock()
232 collection_mock.return_value = vwdmock
233 vwdmock.portable_data_hash.return_value = "99999999999999999999999999999996+99"
239 "class": "InitialWorkDirRequirement",
243 "location": "keep:99999999999999999999999999999995+99/bar"
246 "class": "Directory",
248 "location": "keep:99999999999999999999999999999995+99"
252 "basename": "filename",
253 "location": "keep:99999999999999999999999999999995+99/baz/filename"
256 "class": "Directory",
257 "basename": "subdir",
258 "location": "keep:99999999999999999999999999999995+99/subdir"
263 "class": "CommandLineTool"
266 loadingContext, runtimeContext = self.helper(runner)
267 runtimeContext.name = "test_initial_work_dir"
269 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
270 arvtool.formatgraph = None
271 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
272 j.run(runtimeContext)
274 call_args, call_kwargs = runner.api.container_requests().create.call_args
276 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
277 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
278 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
279 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
281 call_body_expected = {
283 'HOME': '/var/spool/cwl',
286 'name': 'test_initial_work_dir',
287 'runtime_constraints': {
291 'use_existing': True,
294 '/tmp': {'kind': 'tmp',
295 "capacity": 1073741824 },
296 '/var/spool/cwl': {'kind': 'tmp',
297 "capacity": 1073741824 },
298 '/var/spool/cwl/foo': {
299 'kind': 'collection',
301 'portable_data_hash': '99999999999999999999999999999996+99'
303 '/var/spool/cwl/foo2': {
304 'kind': 'collection',
306 'portable_data_hash': '99999999999999999999999999999996+99'
308 '/var/spool/cwl/filename': {
309 'kind': 'collection',
311 'portable_data_hash': '99999999999999999999999999999996+99'
313 '/var/spool/cwl/subdir': {
314 'kind': 'collection',
316 'portable_data_hash': '99999999999999999999999999999996+99'
319 'state': 'Committed',
320 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
321 'output_path': '/var/spool/cwl',
323 'container_image': 'arvados/jobs',
325 'cwd': '/var/spool/cwl',
326 'scheduling_parameters': {
332 call_body = call_kwargs.get('body', None)
333 self.assertNotEqual(None, call_body)
334 for key in call_body:
335 self.assertEqual(call_body_expected.get(key), call_body.get(key))
338 # Test redirecting stdin/stdout/stderr
339 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
340 def test_redirects(self, keepdocker):
341 arv_docker_clear_cache()
343 runner = mock.MagicMock()
344 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
345 runner.ignore_docker_for_reuse = False
346 runner.intermediate_output_ttl = 0
347 runner.secret_store = cwltool.secrets.SecretStore()
349 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
350 runner.api.collections().get().execute.return_value = {
351 "portable_data_hash": "99999999999999999999999999999993+99"}
353 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
359 "stdout": "stdout.txt",
360 "stderr": "stderr.txt",
361 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
362 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
364 "class": "CommandLineTool"
367 loadingContext, runtimeContext = self.helper(runner)
368 runtimeContext.name = "test_run_redirect"
370 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
371 arvtool.formatgraph = None
372 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
373 j.run(runtimeContext)
374 runner.api.container_requests().create.assert_called_with(
375 body=JsonDiffMatcher({
377 'HOME': '/var/spool/cwl',
380 'name': 'test_run_redirect',
381 'runtime_constraints': {
385 'use_existing': True,
388 '/tmp': {'kind': 'tmp',
389 "capacity": 1073741824 },
390 '/var/spool/cwl': {'kind': 'tmp',
391 "capacity": 1073741824 },
394 "path": "/var/spool/cwl/stderr.txt"
397 "kind": "collection",
399 "portable_data_hash": "99999999999999999999999999999996+99"
403 "path": "/var/spool/cwl/stdout.txt"
406 'state': 'Committed',
407 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
408 'output_path': '/var/spool/cwl',
410 'container_image': 'arvados/jobs',
411 'command': ['ls', '/var/spool/cwl'],
412 'cwd': '/var/spool/cwl',
413 'scheduling_parameters': {},
418 @mock.patch("arvados.collection.Collection")
419 def test_done(self, col):
420 api = mock.MagicMock()
422 runner = mock.MagicMock()
424 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
425 runner.num_retries = 0
426 runner.ignore_docker_for_reuse = False
427 runner.intermediate_output_ttl = 0
428 runner.secret_store = cwltool.secrets.SecretStore()
430 runner.api.containers().get().execute.return_value = {"state":"Complete",
434 col().open.return_value = []
436 arvjob = arvados_cwl.ArvadosContainer(runner,
443 arvjob.output_callback = mock.MagicMock()
444 arvjob.collect_outputs = mock.MagicMock()
445 arvjob.successCodes = [0]
446 arvjob.outdir = "/var/spool/cwl"
447 arvjob.output_ttl = 3600
449 arvjob.collect_outputs.return_value = {"out": "stuff"}
453 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
454 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
455 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
456 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
457 "modified_at": "2017-05-26T12:01:22Z"
460 self.assertFalse(api.collections().create.called)
462 arvjob.collect_outputs.assert_called_with("keep:abc+123")
463 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
464 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
466 # The test passes no builder.resources
467 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
468 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
469 def test_mounts(self, keepdocker):
470 arv_docker_clear_cache()
472 runner = mock.MagicMock()
473 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
474 runner.ignore_docker_for_reuse = False
475 runner.intermediate_output_ttl = 0
476 runner.secret_store = cwltool.secrets.SecretStore()
478 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
479 runner.api.collections().get().execute.return_value = {
480 "portable_data_hash": "99999999999999999999999999999993+99"}
482 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
491 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
493 "class": "CommandLineTool"
496 loadingContext, runtimeContext = self.helper(runner)
497 runtimeContext.name = "test_run_mounts"
499 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
500 arvtool.formatgraph = None
503 "class": "Directory",
504 "location": "keep:99999999999999999999999999999994+44",
508 "location": "keep:99999999999999999999999999999994+44/file1",
512 "location": "keep:99999999999999999999999999999994+44/file2",
517 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
518 j.run(runtimeContext)
519 runner.api.container_requests().create.assert_called_with(
520 body=JsonDiffMatcher({
522 'HOME': '/var/spool/cwl',
525 'name': 'test_run_mounts',
526 'runtime_constraints': {
530 'use_existing': True,
533 "/keep/99999999999999999999999999999994+44": {
534 "kind": "collection",
535 "portable_data_hash": "99999999999999999999999999999994+44"
537 '/tmp': {'kind': 'tmp',
538 "capacity": 1073741824 },
539 '/var/spool/cwl': {'kind': 'tmp',
540 "capacity": 1073741824 }
542 'state': 'Committed',
543 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
544 'output_path': '/var/spool/cwl',
546 'container_image': 'arvados/jobs',
547 'command': ['ls', '/var/spool/cwl'],
548 'cwd': '/var/spool/cwl',
549 'scheduling_parameters': {},
554 # The test passes no builder.resources
555 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
556 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
557 def test_secrets(self, keepdocker):
558 arv_docker_clear_cache()
560 runner = mock.MagicMock()
561 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
562 runner.ignore_docker_for_reuse = False
563 runner.intermediate_output_ttl = 0
564 runner.secret_store = cwltool.secrets.SecretStore()
566 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
567 runner.api.collections().get().execute.return_value = {
568 "portable_data_hash": "99999999999999999999999999999993+99"}
570 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
572 tool = cmap({"arguments": ["md5sum", "example.conf"],
573 "class": "CommandLineTool",
576 "class": "http://commonwl.org/cwltool#Secrets",
582 "id": "#secret_job.cwl",
585 "id": "#secret_job.cwl/pw",
593 "class": "InitialWorkDirRequirement",
596 "entry": "username: user\npassword: $(inputs.pw)\n",
597 "entryname": "example.conf"
603 loadingContext, runtimeContext = self.helper(runner)
604 runtimeContext.name = "test_secrets"
606 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
607 arvtool.formatgraph = None
609 job_order = {"pw": "blorp"}
610 runner.secret_store.store(["pw"], job_order)
612 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
613 j.run(runtimeContext)
614 runner.api.container_requests().create.assert_called_with(
615 body=JsonDiffMatcher({
617 'HOME': '/var/spool/cwl',
620 'name': 'test_secrets',
621 'runtime_constraints': {
625 'use_existing': True,
628 '/tmp': {'kind': 'tmp',
629 "capacity": 1073741824
631 '/var/spool/cwl': {'kind': 'tmp',
632 "capacity": 1073741824 }
634 'state': 'Committed',
635 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
636 'output_path': '/var/spool/cwl',
638 'container_image': 'arvados/jobs',
639 'command': ['md5sum', 'example.conf'],
640 'cwd': '/var/spool/cwl',
641 'scheduling_parameters': {},
644 "/var/spool/cwl/example.conf": {
645 "content": "username: user\npassword: blorp\n",
651 # The test passes no builder.resources
652 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
653 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
654 def test_timelimit(self, keepdocker):
655 arv_docker_clear_cache()
657 runner = mock.MagicMock()
658 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
659 runner.ignore_docker_for_reuse = False
660 runner.intermediate_output_ttl = 0
661 runner.secret_store = cwltool.secrets.SecretStore()
663 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
664 runner.api.collections().get().execute.return_value = {
665 "portable_data_hash": "99999999999999999999999999999993+99"}
671 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
673 "class": "CommandLineTool",
676 "class": "http://commonwl.org/cwltool#TimeLimit",
682 loadingContext, runtimeContext = self.helper(runner)
683 runtimeContext.name = "test_timelimit"
685 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
686 arvtool.formatgraph = None
688 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
689 j.run(runtimeContext)
691 _, kwargs = runner.api.container_requests().create.call_args
692 self.assertEqual(42, kwargs['body']['scheduling_parameters'].get('max_run_time'))