Skip to content

Commit 1f2dbf0

Browse files
geetduggaltetron
authored andcommitted
Support for user space docker (#542)
* Support for user space docker * Initial draft of documentation for user space docker
1 parent 7e1cb0e commit 1f2dbf0

File tree

3 files changed

+103
-44
lines changed

3 files changed

+103
-44
lines changed

README.rst

+15
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,18 @@ logger_handler
500500
logging.Handler
501501

502502
Handler object for logging.
503+
504+
Running user-space implementations of Docker
505+
--------------------------------------------
506+
507+
Some compute environments disallow user-space installation of Docker due to incompatiblities in libraries or to meet security requirements. The CWL reference supports using a user space implementation with the `--user-space-docker-cmd` option.
508+
509+
Example using `dx-docker` (https://wiki.dnanexus.com/Developer-Tutorials/Using-Docker-Images):
510+
511+
For use on Linux, install the DNAnexus toolkit (see https://wiki.dnanexus.com/Downloads for instructions).
512+
513+
Run `cwltool` just as you normally would, but with the new option, e.g. from the conformance tests:
514+
515+
```
516+
cwltool --user-space-docker-cmd=dx-docker --outdir=/tmp/tmpidytmp v1.0/test-cwl-out2.cwl v1.0/empty.json
517+
```

cwltool/job.py

+84-44
Original file line numberDiff line numberDiff line change
@@ -321,15 +321,20 @@ def add_volumes(self, pathmapper, runtime):
321321
if not vol.staged:
322322
continue
323323
if vol.target.startswith(container_outdir+"/"):
324-
host_outdir_tgt = os.path.join(host_outdir, vol.target[len(container_outdir)+1:])
324+
host_outdir_tgt = os.path.join(
325+
host_outdir, vol.target[len(container_outdir)+1:])
325326
else:
326327
host_outdir_tgt = None
327328
if vol.type in ("File", "Directory"):
328329
if not vol.resolved.startswith("_:"):
329-
runtime.append(u"--volume=%s:%s:ro" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(vol.target)))
330+
runtime.append(u"--volume=%s:%s:ro" % (
331+
docker_windows_path_adjust(vol.resolved),
332+
docker_windows_path_adjust(vol.target)))
330333
elif vol.type == "WritableFile":
331334
if self.inplace_update:
332-
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(vol.target)))
335+
runtime.append(u"--volume=%s:%s:rw" % (
336+
docker_windows_path_adjust(vol.resolved),
337+
docker_windows_path_adjust(vol.target)))
333338
else:
334339
shutil.copy(vol.resolved, host_outdir_tgt)
335340
ensure_writable(host_outdir_tgt)
@@ -338,7 +343,9 @@ def add_volumes(self, pathmapper, runtime):
338343
os.makedirs(vol.target, 0o0755)
339344
else:
340345
if self.inplace_update:
341-
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(vol.target)))
346+
runtime.append(u"--volume=%s:%s:rw" % (
347+
docker_windows_path_adjust(vol.resolved),
348+
docker_windows_path_adjust(vol.target)))
342349
else:
343350
shutil.copytree(vol.resolved, host_outdir_tgt)
344351
ensure_writable(host_outdir_tgt)
@@ -350,7 +357,9 @@ def add_volumes(self, pathmapper, runtime):
350357
fd, createtmp = tempfile.mkstemp(dir=self.tmpdir)
351358
with os.fdopen(fd, "wb") as f:
352359
f.write(vol.resolved.encode("utf-8"))
353-
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(createtmp), docker_windows_path_adjust(vol.target)))
360+
runtime.append(u"--volume=%s:%s:rw" % (
361+
docker_windows_path_adjust(createtmp),
362+
docker_windows_path_adjust(vol.target)))
354363

355364
def run(self, pull_image=True, rm_container=True,
356365
rm_tmpdir=True, move_outputs="move", **kwargs):
@@ -360,59 +369,89 @@ def run(self, pull_image=True, rm_container=True,
360369

361370
img_id = None
362371
env = None # type: MutableMapping[Text, Text]
363-
try:
364-
env = cast(MutableMapping[Text, Text], os.environ)
365-
if docker_req and kwargs.get("use_container"):
366-
img_id = docker.get_from_requirements(docker_req, True, pull_image)
367-
if img_id is None:
368-
if self.builder.find_default_container:
369-
default_container = self.builder.find_default_container()
370-
if default_container:
371-
img_id = default_container
372-
env = cast(MutableMapping[Text, Text], os.environ)
373-
374-
if docker_req and img_id is None and kwargs.get("use_container"):
375-
raise Exception("Docker image not available")
376-
except Exception as e:
377-
_logger.debug("Docker error", exc_info=True)
378-
if docker_is_req:
379-
raise UnsupportedRequirement(
380-
"Docker is required to run this tool: %s" % e)
372+
user_space_docker_cmd = kwargs.get("user_space_docker_cmd")
373+
if docker_req and user_space_docker_cmd:
374+
# For user-space docker implementations, a local image name or ID
375+
# takes precedence over a network pull
376+
if 'dockerImageId' in docker_req:
377+
img_id = str(docker_req["dockerImageId"])
378+
elif 'dockerPull' in docker_req:
379+
img_id = str(docker_req["dockerPull"])
381380
else:
382-
raise WorkflowException(
383-
"Docker is not available for this tool, try --no-container"
384-
" to disable Docker: %s" % e)
381+
raise Exception("Docker image must be specified as "
382+
"'dockerImageId' or 'dockerPull' when using user "
383+
"space implementations of Docker")
384+
else:
385+
try:
386+
env = cast(MutableMapping[Text, Text], os.environ)
387+
if docker_req and kwargs.get("use_container"):
388+
img_id = str(docker.get_from_requirements(
389+
docker_req, True, pull_image))
390+
if img_id is None:
391+
if self.builder.find_default_container:
392+
default_container = self.builder.find_default_container()
393+
if default_container:
394+
img_id = str(default_container)
395+
env = cast(MutableMapping[Text, Text], os.environ)
396+
397+
if docker_req and img_id is None and kwargs.get("use_container"):
398+
raise Exception("Docker image not available")
399+
except Exception as e:
400+
_logger.debug("Docker error", exc_info=True)
401+
if docker_is_req:
402+
raise UnsupportedRequirement(
403+
"Docker is required to run this tool: %s" % e)
404+
else:
405+
raise WorkflowException(
406+
"Docker is not available for this tool, try "
407+
"--no-container to disable Docker, or install "
408+
"a user space Docker replacement like uDocker with "
409+
"--user-space-docker-cmd.: %s" % e)
385410

386411
self._setup(kwargs)
387412

388-
runtime = [u"docker", u"run", u"-i"]
413+
if user_space_docker_cmd:
414+
runtime = [user_space_docker_cmd, u"run"]
415+
else:
416+
runtime = [u"docker", u"run", u"-i"]
389417

390-
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.outdir)), self.builder.outdir))
391-
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
418+
runtime.append(u"--volume=%s:%s:rw" % (
419+
docker_windows_path_adjust(os.path.realpath(self.outdir)),
420+
self.builder.outdir))
421+
runtime.append(u"--volume=%s:%s:rw" % (
422+
docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
392423

393424
self.add_volumes(self.pathmapper, runtime)
394425
if self.generatemapper:
395426
self.add_volumes(self.generatemapper, runtime)
396427

397-
runtime.append(u"--workdir=%s" % (docker_windows_path_adjust(self.builder.outdir)))
428+
if user_space_docker_cmd:
429+
runtime = [x.replace(":ro", "") for x in runtime]
430+
runtime = [x.replace(":rw", "") for x in runtime]
398431

399-
if not kwargs.get("no_read_only"):
400-
runtime.append(u"--read-only=true")
432+
runtime.append(u"--workdir=%s" % (
433+
docker_windows_path_adjust(self.builder.outdir)))
434+
if not user_space_docker_cmd:
401435

402-
if kwargs.get("custom_net", None) is not None:
403-
runtime.append(u"--net={0}".format(kwargs.get("custom_net")))
404-
elif kwargs.get("disable_net", None):
405-
runtime.append(u"--net=none")
436+
if not kwargs.get("no_read_only"):
437+
runtime.append(u"--read-only=true")
406438

407-
if self.stdout:
408-
runtime.append("--log-driver=none")
439+
if kwargs.get("custom_net", None) is not None:
440+
runtime.append(u"--net={0}".format(kwargs.get("custom_net")))
441+
elif kwargs.get("disable_net", None):
442+
runtime.append(u"--net=none")
443+
444+
if self.stdout:
445+
runtime.append("--log-driver=none")
409446

410-
euid, egid = docker_vm_id()
411-
if not onWindows(): # MS Windows does not have getuid() or geteuid() functions
412-
euid, egid = euid or os.geteuid(), egid or os.getgid()
447+
euid, egid = docker_vm_id()
448+
if not onWindows():
449+
# MS Windows does not have getuid() or geteuid() functions
450+
euid, egid = euid or os.geteuid(), egid or os.getgid()
413451

414-
if kwargs.get("no_match_user", None) is False and (euid, egid) != (None, None):
415-
runtime.append(u"--user=%d:%d" % (euid, egid))
452+
if kwargs.get("no_match_user", None) is False \
453+
and (euid, egid) != (None, None):
454+
runtime.append(u"--user=%d:%d" % (euid, egid))
416455

417456
if rm_container:
418457
runtime.append(u"--rm")
@@ -429,7 +468,8 @@ def run(self, pull_image=True, rm_container=True,
429468

430469
runtime.append(img_id)
431470

432-
self._execute(runtime, env, rm_tmpdir=rm_tmpdir, move_outputs=move_outputs)
471+
self._execute(
472+
runtime, env, rm_tmpdir=rm_tmpdir, move_outputs=move_outputs)
433473

434474

435475
def _job_popen(

cwltool/main.py

+4
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
159159
exgroup.add_argument("--debug", action="store_true", help="Print even more logging")
160160

161161
parser.add_argument("--js-console", action="store_true", help="Enable javascript console output")
162+
parser.add_argument("--user-space-docker-cmd",
163+
help="(Linux/OS X only) Specify a user space docker "
164+
"command (like udocker or dx-docker) that will be "
165+
"used to call 'pull' and 'run'")
162166

163167
dependency_resolvers_configuration_help = argparse.SUPPRESS
164168
dependencies_directory_help = argparse.SUPPRESS

0 commit comments

Comments
 (0)