Skip to content

Commit ddb22ab

Browse files
author
Anton Khodak
committed
1 parent 47d3112 commit ddb22ab

File tree

3 files changed

+180
-43
lines changed

3 files changed

+180
-43
lines changed

cwltool/job.py

Lines changed: 109 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def run(self, pull_image=True, rm_container=True,
313313

314314
class DockerCommandLineJob(JobBase):
315315

316-
def add_volumes(self, pathmapper, runtime):
316+
def add_volumes_docker(self, pathmapper, runtime):
317317
# type: (PathMapper, List[Text]) -> None
318318

319319
host_outdir = self.outdir
@@ -362,6 +362,44 @@ def add_volumes(self, pathmapper, runtime):
362362
docker_windows_path_adjust(createtmp),
363363
docker_windows_path_adjust(vol.target)))
364364

365+
def add_volumes_singularity(self, pathmapper, runtime, stage_output):
366+
# type: (PathMapper, List[Text], bool) -> None
367+
368+
host_outdir = self.outdir
369+
container_outdir = self.builder.outdir
370+
for src, vol in pathmapper.items():
371+
if not vol.staged:
372+
continue
373+
if stage_output:
374+
containertgt = container_outdir + vol.target[len(host_outdir):]
375+
else:
376+
containertgt = vol.target
377+
if vol.type in ("File", "Directory"):
378+
if not vol.resolved.startswith("_:"):
379+
runtime.append(u"--bind")
380+
runtime.append("%s:%s:ro" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
381+
elif vol.type == "WritableFile":
382+
if self.inplace_update:
383+
runtime.append(u"--bind")
384+
runtime.append("%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
385+
else:
386+
shutil.copy(vol.resolved, vol.target)
387+
elif vol.type == "WritableDirectory":
388+
if vol.resolved.startswith("_:"):
389+
os.makedirs(vol.target, 0o0755)
390+
else:
391+
if self.inplace_update:
392+
runtime.append(u"--bind")
393+
runtime.append("%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
394+
else:
395+
shutil.copytree(vol.resolved, vol.target)
396+
elif vol.type == "CreateFile":
397+
createtmp = os.path.join(host_outdir, os.path.basename(vol.target))
398+
with open(createtmp, "wb") as f:
399+
f.write(vol.resolved.encode("utf-8"))
400+
runtime.append(u"--bind")
401+
runtime.append("%s:%s:ro" % (docker_windows_path_adjust(createtmp), docker_windows_path_adjust(vol.target)))
402+
365403
def run(self, pull_image=True, rm_container=True,
366404
rm_tmpdir=True, move_outputs="move", **kwargs):
367405
# type: (bool, bool, bool, Text, **Any) -> None
@@ -371,6 +409,7 @@ def run(self, pull_image=True, rm_container=True,
371409
img_id = None
372410
env = None # type: MutableMapping[Text, Text]
373411
user_space_docker_cmd = kwargs.get("user_space_docker_cmd")
412+
container_manager = kwargs.get("container_manager")
374413
if docker_req and user_space_docker_cmd:
375414
# For user-space docker implementations, a local image name or ID
376415
# takes precedence over a network pull
@@ -411,61 +450,88 @@ def run(self, pull_image=True, rm_container=True,
411450

412451
self._setup(kwargs)
413452

414-
if user_space_docker_cmd:
415-
runtime = [user_space_docker_cmd, u"run"]
416-
else:
417-
runtime = [u"docker", u"run", u"-i"]
453+
if container_manager == "docker":
454+
if user_space_docker_cmd:
455+
runtime = [user_space_docker_cmd, u"run"]
456+
else:
457+
runtime = [u"docker", u"run", u"-i"]
418458

419-
runtime.append(u"--volume=%s:%s:rw" % (
420-
docker_windows_path_adjust(os.path.realpath(self.outdir)),
421-
self.builder.outdir))
422-
runtime.append(u"--volume=%s:%s:rw" % (
423-
docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
459+
runtime.append(u"--volume=%s:%s:rw" % (
460+
docker_windows_path_adjust(os.path.realpath(self.outdir)),
461+
self.builder.outdir))
462+
runtime.append(u"--volume=%s:%s:rw" % (
463+
docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
424464

425-
self.add_volumes(self.pathmapper, runtime)
426-
if self.generatemapper:
427-
self.add_volumes(self.generatemapper, runtime)
465+
self.add_volumes_docker(self.pathmapper, runtime)
466+
if self.generatemapper:
467+
self.add_volumes_docker(self.generatemapper, runtime)
428468

429-
if user_space_docker_cmd:
430-
runtime = [x.replace(":ro", "") for x in runtime]
431-
runtime = [x.replace(":rw", "") for x in runtime]
469+
if user_space_docker_cmd:
470+
runtime = [x.replace(":ro", "") for x in runtime]
471+
runtime = [x.replace(":rw", "") for x in runtime]
432472

433-
runtime.append(u"--workdir=%s" % (
434-
docker_windows_path_adjust(self.builder.outdir)))
435-
if not user_space_docker_cmd:
473+
runtime.append(u"--workdir=%s" % (
474+
docker_windows_path_adjust(self.builder.outdir)))
475+
if not user_space_docker_cmd:
436476

437-
if not kwargs.get("no_read_only"):
438-
runtime.append(u"--read-only=true")
477+
if not kwargs.get("no_read_only"):
478+
runtime.append(u"--read-only=true")
439479

440-
if kwargs.get("custom_net", None) is not None:
441-
runtime.append(u"--net={0}".format(kwargs.get("custom_net")))
442-
elif kwargs.get("disable_net", None):
443-
runtime.append(u"--net=none")
480+
if kwargs.get("custom_net", None) is not None:
481+
runtime.append(u"--net={0}".format(kwargs.get("custom_net")))
482+
elif kwargs.get("disable_net", None):
483+
runtime.append(u"--net=none")
444484

445-
if self.stdout:
446-
runtime.append("--log-driver=none")
485+
if self.stdout:
486+
runtime.append("--log-driver=none")
487+
488+
euid, egid = docker_vm_id()
489+
if not onWindows():
490+
# MS Windows does not have getuid() or geteuid() functions
491+
euid, egid = euid or os.geteuid(), egid or os.getgid()
492+
493+
if kwargs.get("no_match_user", None) is False \
494+
and (euid, egid) != (None, None):
495+
runtime.append(u"--user=%d:%d" % (euid, egid))
496+
497+
if rm_container:
498+
runtime.append(u"--rm")
447499

448-
euid, egid = docker_vm_id()
449-
if not onWindows():
450-
# MS Windows does not have getuid() or geteuid() functions
451-
euid, egid = euid or os.geteuid(), egid or os.getgid()
500+
runtime.append(u"--env=TMPDIR=/tmp")
452501

453-
if kwargs.get("no_match_user", None) is False \
454-
and (euid, egid) != (None, None):
455-
runtime.append(u"--user=%d:%d" % (euid, egid))
502+
# spec currently says "HOME must be set to the designated output
503+
# directory." but spec might change to designated temp directory.
504+
# runtime.append("--env=HOME=/tmp")
505+
runtime.append(u"--env=HOME=%s" % self.builder.outdir)
456506

457-
if rm_container:
458-
runtime.append(u"--rm")
507+
for t, v in self.environment.items():
508+
runtime.append(u"--env=%s=%s" % (t, v))
509+
elif container_manager == "singularity":
510+
runtime = [u"singularity", u"exec"]
459511

460-
runtime.append(u"--env=TMPDIR=/tmp")
512+
runtime.append(u"--bind")
513+
runtime.append(
514+
u"%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.outdir)), self.builder.outdir))
515+
runtime.append(u"--bind")
516+
runtime.append(u"%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
517+
518+
self.add_volumes_singularity(self.pathmapper, runtime, False)
519+
if self.generatemapper:
520+
self.add_volumes_singularity(self.generatemapper, runtime, True)
521+
522+
runtime.append(u"--pwd")
523+
runtime.append("%s" % (docker_windows_path_adjust(self.builder.outdir)))
524+
# runtime.append(u"--read-only=true") # true by default for Singularity images
525+
526+
if kwargs.get("custom_net", None) is not None:
527+
raise UnsupportedRequirement(
528+
"Singularity implementation does not support networking")
461529

462-
# spec currently says "HOME must be set to the designated output
463-
# directory." but spec might change to designated temp directory.
464-
# runtime.append("--env=HOME=/tmp")
465-
runtime.append(u"--env=HOME=%s" % self.builder.outdir)
530+
env["SINGULARITYENV_TMPDIR"] = "/tmp"
531+
env["SINGULARITYENV_HOME"] = self.builder.outdir
466532

467-
for t, v in self.environment.items():
468-
runtime.append(u"--env=%s=%s" % (t, v))
533+
for t, v in self.environment.items():
534+
env["SINGULARITYENV_" + t] = v
469535

470536
runtime.append(img_id)
471537

cwltool/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
6363
parser.add_argument("--no-container", action="store_false", default=True,
6464
help="Do not execute jobs in a Docker container, even when specified by the CommandLineTool",
6565
dest="use_container")
66+
parser.add_argument("--container-manager", choices={"docker", "singularity"}, default="docker",
67+
help="Container manager, default: docker")
6668

6769
parser.add_argument("--preserve-environment", type=Text, action="append",
6870
help="Preserve specific environment variable when running CommandLineTools. May be provided multiple times.",

cwltool/singularity.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from __future__ import absolute_import
2+
import logging
3+
import os
4+
import re
5+
import subprocess
6+
import sys
7+
from typing import Dict, List, Text
8+
9+
from .errors import WorkflowException
10+
11+
_logger = logging.getLogger("cwltool")
12+
13+
14+
def get_image(dockerRequirement, pull_image, dry_run=False):
15+
# type: (Dict[Text, Text], bool, bool) -> bool
16+
found = False
17+
18+
if "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
19+
match = re.search(pattern=r'([a-z]*://)',string=dockerRequirement["dockerPull"])
20+
if match:
21+
dockerRequirement["dockerImageId"] = re.sub(pattern=r'([a-z]*://)', repl=r'', string=dockerRequirement["dockerPull"])
22+
dockerRequirement["dockerImageId"] = re.sub(pattern=r'[:/]', repl=r'-', string=dockerRequirement["dockerImageId"]) + ".img"
23+
else:
24+
dockerRequirement["dockerImageId"] = re.sub(pattern=r'[:/]', repl=r'-', string=dockerRequirement["dockerPull"]) + ".img"
25+
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
26+
27+
# check to see if the Singularity container is already downloaded
28+
if os.path.isfile(dockerRequirement["dockerImageId"]):
29+
_logger.info("Using local copy of Singularity image")
30+
found = True
31+
32+
# if the .img file is not already present, pull the image
33+
elif pull_image:
34+
cmd = [] # type: List[Text]
35+
if "dockerPull" in dockerRequirement:
36+
cmd = ["singularity", "pull", "--name", str(dockerRequirement["dockerImageId"]), str(dockerRequirement["dockerPull"])]
37+
_logger.info(Text(cmd))
38+
if not dry_run:
39+
subprocess.check_call(cmd, stdout=sys.stderr)
40+
found = True
41+
42+
return found
43+
44+
45+
def get_from_requirements(r, req, pull_image, dry_run=False):
46+
# type: (Dict[Text, Text], bool, bool, bool) -> Text
47+
# returns the filename of the Singularity image (e.g. hello-world-latest.img)
48+
if r:
49+
errmsg = None
50+
try:
51+
subprocess.check_output(["singularity", "--version"])
52+
except subprocess.CalledProcessError as e:
53+
errmsg = "Cannot execute 'singularity --version' " + Text(e)
54+
except OSError as e:
55+
errmsg = "'singularity' executable not found: " + Text(e)
56+
57+
if errmsg:
58+
if req:
59+
raise WorkflowException(errmsg)
60+
else:
61+
return None
62+
63+
if get_image(r, pull_image, dry_run):
64+
return os.path.abspath(r["dockerImageId"])
65+
else:
66+
if req:
67+
raise WorkflowException(u"Container image %s not found" % r["dockerImageId"])
68+
69+
return None

0 commit comments

Comments
 (0)