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)
25 class TestContainer(unittest.TestCase):
27 def helper(self, runner, enable_reuse=True):
28 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
30 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
31 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
32 loadingContext = arvados_cwl.context.ArvLoadingContext(
33 {"avsc_names": avsc_names,
35 "make_fs_access": make_fs_access,
37 "metadata": {"cwlVersion": "v1.0"}})
38 runtimeContext = arvados_cwl.context.ArvRuntimeContext(
39 {"work_api": "containers",
41 "name": "test_run_"+str(enable_reuse),
42 "make_fs_access": make_fs_access,
44 "enable_reuse": enable_reuse,
47 return loadingContext, runtimeContext
49 # The test passes no builder.resources
50 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
51 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
52 def test_run(self, keepdocker):
53 for enable_reuse in (True, False):
54 arv_docker_clear_cache()
56 runner = mock.MagicMock()
57 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
58 runner.ignore_docker_for_reuse = False
59 runner.intermediate_output_ttl = 0
60 runner.secret_store = cwltool.secrets.SecretStore()
62 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
63 runner.api.collections().get().execute.return_value = {
64 "portable_data_hash": "99999999999999999999999999999993+99"}
70 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
72 "class": "CommandLineTool"
75 loadingContext, runtimeContext = self.helper(runner, enable_reuse)
77 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
78 arvtool.formatgraph = None
80 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
82 runner.api.container_requests().create.assert_called_with(
83 body=JsonDiffMatcher({
85 'HOME': '/var/spool/cwl',
88 'name': 'test_run_'+str(enable_reuse),
89 'runtime_constraints': {
93 'use_existing': enable_reuse,
96 '/tmp': {'kind': 'tmp',
97 "capacity": 1073741824
99 '/var/spool/cwl': {'kind': 'tmp',
100 "capacity": 1073741824 }
102 'state': 'Committed',
103 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
104 'output_path': '/var/spool/cwl',
106 'container_image': 'arvados/jobs',
107 'command': ['ls', '/var/spool/cwl'],
108 'cwd': '/var/spool/cwl',
109 'scheduling_parameters': {},
114 # The test passes some fields in builder.resources
115 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
116 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
117 def test_resource_requirements(self, keepdocker):
118 arv_docker_clear_cache()
119 runner = mock.MagicMock()
120 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
121 runner.ignore_docker_for_reuse = False
122 runner.intermediate_output_ttl = 3600
123 runner.secret_store = cwltool.secrets.SecretStore()
125 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
126 runner.api.collections().get().execute.return_value = {
127 "portable_data_hash": "99999999999999999999999999999993+99"}
133 "class": "ResourceRequirement",
139 "class": "http://arvados.org/cwl#RuntimeConstraints",
142 "class": "http://arvados.org/cwl#APIRequirement",
144 "class": "http://arvados.org/cwl#PartitionRequirement",
147 "class": "http://arvados.org/cwl#IntermediateOutput",
150 "class": "http://arvados.org/cwl#ReuseRequirement",
155 "class": "CommandLineTool"
158 loadingContext, runtimeContext = self.helper(runner)
159 runtimeContext.name = "test_resource_requirements"
161 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
162 arvtool.formatgraph = None
163 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
164 j.run(runtimeContext)
166 call_args, call_kwargs = runner.api.container_requests().create.call_args
168 call_body_expected = {
170 'HOME': '/var/spool/cwl',
173 'name': 'test_resource_requirements',
174 'runtime_constraints': {
177 'keep_cache_ram': 536870912,
180 'use_existing': False,
183 '/tmp': {'kind': 'tmp',
184 "capacity": 4194304000 },
185 '/var/spool/cwl': {'kind': 'tmp',
186 "capacity": 5242880000 }
188 'state': 'Committed',
189 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
190 'output_path': '/var/spool/cwl',
192 'container_image': 'arvados/jobs',
194 'cwd': '/var/spool/cwl',
195 'scheduling_parameters': {
196 'partitions': ['blurb']
202 call_body = call_kwargs.get('body', None)
203 self.assertNotEqual(None, call_body)
204 for key in call_body:
205 self.assertEqual(call_body_expected.get(key), call_body.get(key))
208 # The test passes some fields in builder.resources
209 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
210 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
211 @mock.patch("arvados.collection.Collection")
212 def test_initial_work_dir(self, collection_mock, keepdocker):
213 arv_docker_clear_cache()
214 runner = mock.MagicMock()
215 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
216 runner.ignore_docker_for_reuse = False
217 runner.intermediate_output_ttl = 0
218 runner.secret_store = cwltool.secrets.SecretStore()
220 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
221 runner.api.collections().get().execute.return_value = {
222 "portable_data_hash": "99999999999999999999999999999993+99"}
224 sourcemock = mock.MagicMock()
225 def get_collection_mock(p):
227 return (sourcemock, p.split("/", 1)[1])
229 return (sourcemock, "")
230 runner.fs_access.get_collection.side_effect = get_collection_mock
232 vwdmock = mock.MagicMock()
233 collection_mock.return_value = vwdmock
234 vwdmock.portable_data_hash.return_value = "99999999999999999999999999999996+99"
240 "class": "InitialWorkDirRequirement",
244 "location": "keep:99999999999999999999999999999995+99/bar"
247 "class": "Directory",
249 "location": "keep:99999999999999999999999999999995+99"
253 "basename": "filename",
254 "location": "keep:99999999999999999999999999999995+99/baz/filename"
257 "class": "Directory",
258 "basename": "subdir",
259 "location": "keep:99999999999999999999999999999995+99/subdir"
264 "class": "CommandLineTool"
267 loadingContext, runtimeContext = self.helper(runner)
268 runtimeContext.name = "test_initial_work_dir"
270 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
271 arvtool.formatgraph = None
272 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
273 j.run(runtimeContext)
275 call_args, call_kwargs = runner.api.container_requests().create.call_args
277 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
278 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
279 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
280 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
282 call_body_expected = {
284 'HOME': '/var/spool/cwl',
287 'name': 'test_initial_work_dir',
288 'runtime_constraints': {
292 'use_existing': True,
295 '/tmp': {'kind': 'tmp',
296 "capacity": 1073741824 },
297 '/var/spool/cwl': {'kind': 'tmp',
298 "capacity": 1073741824 },
299 '/var/spool/cwl/foo': {
300 'kind': 'collection',
302 'portable_data_hash': '99999999999999999999999999999996+99'
304 '/var/spool/cwl/foo2': {
305 'kind': 'collection',
307 'portable_data_hash': '99999999999999999999999999999996+99'
309 '/var/spool/cwl/filename': {
310 'kind': 'collection',
312 'portable_data_hash': '99999999999999999999999999999996+99'
314 '/var/spool/cwl/subdir': {
315 'kind': 'collection',
317 'portable_data_hash': '99999999999999999999999999999996+99'
320 'state': 'Committed',
321 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
322 'output_path': '/var/spool/cwl',
324 'container_image': 'arvados/jobs',
326 'cwd': '/var/spool/cwl',
327 'scheduling_parameters': {
333 call_body = call_kwargs.get('body', None)
334 self.assertNotEqual(None, call_body)
335 for key in call_body:
336 self.assertEqual(call_body_expected.get(key), call_body.get(key))
339 # Test redirecting stdin/stdout/stderr
340 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
341 def test_redirects(self, keepdocker):
342 arv_docker_clear_cache()
344 runner = mock.MagicMock()
345 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
346 runner.ignore_docker_for_reuse = False
347 runner.intermediate_output_ttl = 0
348 runner.secret_store = cwltool.secrets.SecretStore()
350 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
351 runner.api.collections().get().execute.return_value = {
352 "portable_data_hash": "99999999999999999999999999999993+99"}
354 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
360 "stdout": "stdout.txt",
361 "stderr": "stderr.txt",
362 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
363 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
365 "class": "CommandLineTool"
368 loadingContext, runtimeContext = self.helper(runner)
369 runtimeContext.name = "test_run_redirect"
371 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
372 arvtool.formatgraph = None
373 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
374 j.run(runtimeContext)
375 runner.api.container_requests().create.assert_called_with(
376 body=JsonDiffMatcher({
378 'HOME': '/var/spool/cwl',
381 'name': 'test_run_redirect',
382 'runtime_constraints': {
386 'use_existing': True,
389 '/tmp': {'kind': 'tmp',
390 "capacity": 1073741824 },
391 '/var/spool/cwl': {'kind': 'tmp',
392 "capacity": 1073741824 },
395 "path": "/var/spool/cwl/stderr.txt"
398 "kind": "collection",
400 "portable_data_hash": "99999999999999999999999999999996+99"
404 "path": "/var/spool/cwl/stdout.txt"
407 'state': 'Committed',
408 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
409 'output_path': '/var/spool/cwl',
411 'container_image': 'arvados/jobs',
412 'command': ['ls', '/var/spool/cwl'],
413 'cwd': '/var/spool/cwl',
414 'scheduling_parameters': {},
419 @mock.patch("arvados.collection.Collection")
420 def test_done(self, col):
421 api = mock.MagicMock()
423 runner = mock.MagicMock()
425 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
426 runner.num_retries = 0
427 runner.ignore_docker_for_reuse = False
428 runner.intermediate_output_ttl = 0
429 runner.secret_store = cwltool.secrets.SecretStore()
431 runner.api.containers().get().execute.return_value = {"state":"Complete",
435 col().open.return_value = []
437 arvjob = arvados_cwl.ArvadosContainer(runner,
444 arvjob.output_callback = mock.MagicMock()
445 arvjob.collect_outputs = mock.MagicMock()
446 arvjob.successCodes = [0]
447 arvjob.outdir = "/var/spool/cwl"
448 arvjob.output_ttl = 3600
450 arvjob.collect_outputs.return_value = {"out": "stuff"}
454 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
455 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
456 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
457 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
458 "modified_at": "2017-05-26T12:01:22Z"
461 self.assertFalse(api.collections().create.called)
463 arvjob.collect_outputs.assert_called_with("keep:abc+123")
464 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
465 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
467 # The test passes no builder.resources
468 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
469 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
470 def test_mounts(self, keepdocker):
471 arv_docker_clear_cache()
473 runner = mock.MagicMock()
474 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
475 runner.ignore_docker_for_reuse = False
476 runner.intermediate_output_ttl = 0
477 runner.secret_store = cwltool.secrets.SecretStore()
479 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
480 runner.api.collections().get().execute.return_value = {
481 "portable_data_hash": "99999999999999999999999999999993+99"}
483 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
492 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
494 "class": "CommandLineTool"
497 loadingContext, runtimeContext = self.helper(runner)
498 runtimeContext.name = "test_run_mounts"
500 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
501 arvtool.formatgraph = None
504 "class": "Directory",
505 "location": "keep:99999999999999999999999999999994+44",
509 "location": "keep:99999999999999999999999999999994+44/file1",
513 "location": "keep:99999999999999999999999999999994+44/file2",
518 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
519 j.run(runtimeContext)
520 runner.api.container_requests().create.assert_called_with(
521 body=JsonDiffMatcher({
523 'HOME': '/var/spool/cwl',
526 'name': 'test_run_mounts',
527 'runtime_constraints': {
531 'use_existing': True,
534 "/keep/99999999999999999999999999999994+44": {
535 "kind": "collection",
536 "portable_data_hash": "99999999999999999999999999999994+44"
538 '/tmp': {'kind': 'tmp',
539 "capacity": 1073741824 },
540 '/var/spool/cwl': {'kind': 'tmp',
541 "capacity": 1073741824 }
543 'state': 'Committed',
544 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
545 'output_path': '/var/spool/cwl',
547 'container_image': 'arvados/jobs',
548 'command': ['ls', '/var/spool/cwl'],
549 'cwd': '/var/spool/cwl',
550 'scheduling_parameters': {},
555 # The test passes no builder.resources
556 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
557 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
558 def test_secrets(self, keepdocker):
559 arv_docker_clear_cache()
561 runner = mock.MagicMock()
562 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
563 runner.ignore_docker_for_reuse = False
564 runner.intermediate_output_ttl = 0
565 runner.secret_store = cwltool.secrets.SecretStore()
567 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
568 runner.api.collections().get().execute.return_value = {
569 "portable_data_hash": "99999999999999999999999999999993+99"}
571 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
573 tool = cmap({"arguments": ["md5sum", "example.conf"],
574 "class": "CommandLineTool",
577 "class": "http://commonwl.org/cwltool#Secrets",
583 "id": "#secret_job.cwl",
586 "id": "#secret_job.cwl/pw",
594 "class": "InitialWorkDirRequirement",
597 "entry": "username: user\npassword: $(inputs.pw)\n",
598 "entryname": "example.conf"
604 loadingContext, runtimeContext = self.helper(runner)
605 runtimeContext.name = "test_secrets"
607 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
608 arvtool.formatgraph = None
610 job_order = {"pw": "blorp"}
611 runner.secret_store.store(["pw"], job_order)
613 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
614 j.run(runtimeContext)
615 runner.api.container_requests().create.assert_called_with(
616 body=JsonDiffMatcher({
618 'HOME': '/var/spool/cwl',
621 'name': 'test_secrets',
622 'runtime_constraints': {
626 'use_existing': True,
629 '/tmp': {'kind': 'tmp',
630 "capacity": 1073741824
632 '/var/spool/cwl': {'kind': 'tmp',
633 "capacity": 1073741824 }
635 'state': 'Committed',
636 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
637 'output_path': '/var/spool/cwl',
639 'container_image': 'arvados/jobs',
640 'command': ['md5sum', 'example.conf'],
641 'cwd': '/var/spool/cwl',
642 'scheduling_parameters': {},
645 "/var/spool/cwl/example.conf": {
646 "content": "username: user\npassword: blorp\n",
652 # The test passes no builder.resources
653 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
654 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
655 def test_timelimit(self, keepdocker):
656 arv_docker_clear_cache()
658 runner = mock.MagicMock()
659 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
660 runner.ignore_docker_for_reuse = False
661 runner.intermediate_output_ttl = 0
662 runner.secret_store = cwltool.secrets.SecretStore()
664 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
665 runner.api.collections().get().execute.return_value = {
666 "portable_data_hash": "99999999999999999999999999999993+99"}
672 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
674 "class": "CommandLineTool",
677 "class": "http://commonwl.org/cwltool#TimeLimit",
683 loadingContext, runtimeContext = self.helper(runner)
684 runtimeContext.name = "test_timelimit"
686 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
687 arvtool.formatgraph = None
689 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
690 j.run(runtimeContext)
692 _, kwargs = runner.api.container_requests().create.call_args
693 self.assertEqual(42, kwargs['body']['scheduling_parameters'].get('max_run_time'))