Skip to content

Commit 1424504

Browse files
authored
Merge pull request #935 from common-workflow-language/singularity-iwdr
workaround for Singularity + InitialWorkDirRequriement
2 parents d5326de + 8f53dcf commit 1424504

13 files changed

+632
-163
lines changed

cwltool/docker.py

Lines changed: 82 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from .errors import WorkflowException
2121
from .job import ContainerCommandLineJob
2222
from .loghandler import _logger
23-
from .pathmapper import PathMapper # pylint: disable=unused-import
24-
from .pathmapper import ensure_writable
23+
from .pathmapper import PathMapper, MapperEnt # pylint: disable=unused-import
24+
from .pathmapper import ensure_writable, ensure_non_writable
2525
from .secrets import SecretStore # pylint: disable=unused-import
2626
from .utils import (DEFAULT_TMP_PREFIX, docker_windows_path_adjust, onWindows,
2727
subprocess)
@@ -208,83 +208,85 @@ def get_from_requirements(self,
208208

209209
return None
210210

211-
def add_volumes(self, pathmapper, runtime, secret_store=None):
212-
# type: (PathMapper, List[Text], SecretStore) -> None
213-
"""Append volume mappings to the runtime option list."""
214-
215-
host_outdir = self.outdir
216-
container_outdir = self.builder.outdir
217-
for _, vol in pathmapper.items():
218-
if not vol.staged:
219-
continue
220-
host_outdir_tgt = None # type: Optional[Text]
221-
if vol.target.startswith(container_outdir + "/"):
222-
host_outdir_tgt = os.path.join(
223-
host_outdir, vol.target[len(container_outdir)+1:])
224-
225-
if vol.type in ("File", "Directory"):
226-
if not vol.resolved.startswith("_:"):
227-
_check_docker_machine_path(docker_windows_path_adjust(
228-
vol.resolved))
229-
runtime.append(u"--volume=%s:%s:ro" % (
230-
docker_windows_path_adjust(vol.resolved),
231-
docker_windows_path_adjust(vol.target)))
232-
elif vol.type == "WritableFile":
233-
if self.inplace_update:
234-
runtime.append(u"--volume=%s:%s:rw" % (
235-
docker_windows_path_adjust(vol.resolved),
236-
docker_windows_path_adjust(vol.target)))
237-
else:
238-
if host_outdir_tgt:
239-
shutil.copy(vol.resolved, host_outdir_tgt)
240-
ensure_writable(host_outdir_tgt)
241-
else:
242-
raise WorkflowException(
243-
"Unable to compute host_outdir_tgt for "
244-
"WriteableFile.")
245-
elif vol.type == "WritableDirectory":
246-
if vol.resolved.startswith("_:"):
247-
if host_outdir_tgt:
248-
if not os.path.exists(host_outdir_tgt):
249-
os.makedirs(host_outdir_tgt, 0o0755)
250-
else:
251-
raise WorkflowException(
252-
"Unable to compute host_outdir_tgt for "
253-
"WritableDirectory.")
254-
else:
255-
if self.inplace_update:
256-
runtime.append(u"--volume=%s:%s:rw" % (
257-
docker_windows_path_adjust(vol.resolved),
258-
docker_windows_path_adjust(vol.target)))
259-
else:
260-
if host_outdir_tgt:
261-
shutil.copytree(vol.resolved, host_outdir_tgt)
262-
ensure_writable(host_outdir_tgt)
263-
else:
264-
raise WorkflowException(
265-
"Unable to compute host_outdir_tgt for "
266-
"WritableDirectory.")
267-
elif vol.type == "CreateFile":
268-
if secret_store:
269-
contents = secret_store.retrieve(vol.resolved)
270-
else:
271-
contents = vol.resolved
272-
if host_outdir_tgt:
273-
dirname = os.path.dirname(host_outdir_tgt)
274-
if not os.path.exists(dirname):
275-
os.makedirs(dirname, 0o0755)
276-
with open(host_outdir_tgt, "wb") as file_literal:
277-
file_literal.write(contents.encode("utf-8"))
211+
@staticmethod
212+
def append_volume(runtime, source, target, writable=False):
213+
# type: (List[Text], Text, Text, bool) -> None
214+
"""Add binding arguments to the runtime list."""
215+
runtime.append(u"--volume={}:{}:{}".format(
216+
docker_windows_path_adjust(source),
217+
docker_windows_path_adjust(target), "rw" if writable else "ro"))
218+
219+
def add_file_or_directory_volume(self,
220+
runtime, # type: List[Text]
221+
volume, # type: MapperEnt
222+
host_outdir_tgt # type: Optional[Text]
223+
): # type: (...) -> None
224+
"""Append volume a file/dir mapping to the runtime option list."""
225+
if not volume.resolved.startswith("_:"):
226+
_check_docker_machine_path(docker_windows_path_adjust(
227+
volume.resolved))
228+
self.append_volume(runtime, volume.resolved, volume.target)
229+
230+
def add_writable_file_volume(self,
231+
runtime, # type: List[Text]
232+
volume, # type: MapperEnt
233+
host_outdir_tgt # type: Optional[Text]
234+
): # type: (...) -> None
235+
"""Append a writable file mapping to the runtime option list."""
236+
if self.inplace_update:
237+
self.append_volume(runtime, volume.resolved, volume.target,
238+
writable=True)
239+
else:
240+
if host_outdir_tgt:
241+
# shortcut, just copy to the output directory
242+
# which is already going to be mounted
243+
shutil.copy(volume.resolved, host_outdir_tgt)
244+
else:
245+
tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
246+
file_copy = os.path.join(
247+
tmpdir, os.path.basename(volume.resolved))
248+
shutil.copy(volume.resolved, file_copy)
249+
self.append_volume(runtime, file_copy, volume.target,
250+
writable=True)
251+
ensure_writable(host_outdir_tgt or file_copy)
252+
253+
def add_writable_directory_volume(self,
254+
runtime, # type: List[Text]
255+
volume, # type: MapperEnt
256+
host_outdir_tgt # type: Optional[Text]
257+
): # type: (...) -> None
258+
"""Append a writable directory mapping to the runtime option list."""
259+
if volume.resolved.startswith("_:"):
260+
# Synthetic directory that needs creating first
261+
if not host_outdir_tgt:
262+
new_dir = os.path.join(
263+
tempfile.mkdtemp(dir=self.tmpdir),
264+
os.path.basename(volume.target))
265+
self.append_volume(runtime, new_dir, volume.target,
266+
writable=True)
267+
elif not os.path.exists(host_outdir_tgt):
268+
os.makedirs(host_outdir_tgt, 0o0755)
269+
else:
270+
if self.inplace_update:
271+
self.append_volume(runtime, volume.resolved, volume.target,
272+
writable=True)
273+
else:
274+
if not host_outdir_tgt:
275+
tmpdir = tempfile.mkdtemp(dir=self.tmpdir)
276+
new_dir = os.path.join(
277+
tmpdir, os.path.basename(volume.resolved))
278+
shutil.copytree(volume.resolved, new_dir)
279+
self.append_volume(
280+
runtime, new_dir, volume.target,
281+
writable=True)
278282
else:
279-
tmp_fd, createtmp = tempfile.mkstemp(dir=self.tmpdir)
280-
with os.fdopen(tmp_fd, "wb") as file_literal:
281-
file_literal.write(contents.encode("utf-8"))
282-
runtime.append(u"--volume=%s:%s:rw" % (
283-
docker_windows_path_adjust(os.path.realpath(createtmp)),
284-
vol.target))
283+
shutil.copytree(volume.resolved, host_outdir_tgt)
284+
ensure_writable(host_outdir_tgt or new_dir)
285285

286286
def create_runtime(self, env, runtimeContext):
287287
# type: (MutableMapping[Text, Text], RuntimeContext) -> List
288+
any_path_okay = self.builder.get_requirement("DockerRequirement")[1] \
289+
or False
288290
user_space_docker_cmd = runtimeContext.user_space_docker_cmd
289291
if user_space_docker_cmd:
290292
if 'udocker' in user_space_docker_cmd and not runtimeContext.debug:
@@ -302,9 +304,12 @@ def create_runtime(self, env, runtimeContext):
302304
runtime.append(u"--volume=%s:%s:rw" % (
303305
docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
304306

305-
self.add_volumes(self.pathmapper, runtime, secret_store=runtimeContext.secret_store)
307+
self.add_volumes(self.pathmapper, runtime, any_path_okay=True,
308+
secret_store=runtimeContext.secret_store)
306309
if self.generatemapper:
307-
self.add_volumes(self.generatemapper, runtime, secret_store=runtimeContext.secret_store)
310+
self.add_volumes(
311+
self.generatemapper, runtime, any_path_okay=any_path_okay,
312+
secret_store=runtimeContext.secret_store)
308313

309314
if user_space_docker_cmd:
310315
runtime = [x.replace(":ro", "") for x in runtime]

cwltool/job.py

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
from .context import getdefault
3030
from .errors import WorkflowException
3131
from .loghandler import _logger
32-
from .pathmapper import PathMapper
32+
from .pathmapper import (MapperEnt, PathMapper, ensure_writable,
33+
ensure_non_writable)
3334
from .process import UnsupportedRequirement, stageFiles
3435
from .secrets import SecretStore # pylint: disable=unused-import
3536
from .utils import \
@@ -122,20 +123,35 @@ def deref_links(outputs): # type: (Any) -> None
122123
for output in outputs:
123124
deref_links(output)
124125

125-
def relink_initialworkdir(pathmapper, host_outdir, container_outdir, inplace_update=False):
126-
# type: (PathMapper, Text, Text, bool) -> None
126+
def relink_initialworkdir(pathmapper, # type: PathMapper
127+
host_outdir, # type: Text
128+
container_outdir, # type: Text
129+
inplace_update=False # type: bool
130+
): # type: (...) -> None
127131
for _, vol in pathmapper.items():
128132
if not vol.staged:
129133
continue
130134

131-
if vol.type in ("File", "Directory") or (inplace_update and
132-
vol.type in ("WritableFile", "WritableDirectory")):
133-
host_outdir_tgt = os.path.join(host_outdir, vol.target[len(container_outdir)+1:])
134-
if os.path.islink(host_outdir_tgt) or os.path.isfile(host_outdir_tgt):
135+
if (vol.type in ("File", "Directory") or (
136+
inplace_update and vol.type in
137+
("WritableFile", "WritableDirectory"))):
138+
if not vol.target.startswith(container_outdir):
139+
# this is an input file written outside of the working
140+
# directory, so therefor ineligable for being an output file.
141+
# Thus, none of our business
142+
continue
143+
host_outdir_tgt = os.path.join(
144+
host_outdir, vol.target[len(container_outdir)+1:])
145+
if os.path.islink(host_outdir_tgt) \
146+
or os.path.isfile(host_outdir_tgt):
135147
os.remove(host_outdir_tgt)
136-
elif os.path.isdir(host_outdir_tgt) and not vol.resolved.startswith("_:"):
148+
elif os.path.isdir(host_outdir_tgt) \
149+
and not vol.resolved.startswith("_:"):
137150
shutil.rmtree(host_outdir_tgt)
138151
if onWindows():
152+
# If this becomes a big issue for someone then we could
153+
# refactor the code to process output from a running container
154+
# and avoid all the extra IO below
139155
if vol.type in ("File", "WritableFile"):
140156
shutil.copy(vol.resolved, host_outdir_tgt)
141157
elif vol.type in ("Directory", "WritableDirectory"):
@@ -427,6 +443,102 @@ def create_runtime(self, env, runtimeContext):
427443
""" Return the list of commands to run the selected container engine."""
428444
pass
429445

446+
@staticmethod
447+
@abstractmethod
448+
def append_volume(runtime, source, target, writable=False):
449+
# type: (List[Text], Text, Text, bool) -> None
450+
"""Add binding arguments to the runtime list."""
451+
pass
452+
453+
@abstractmethod
454+
def add_file_or_directory_volume(self,
455+
runtime, # type: List[Text]
456+
volume, # type: MapperEnt
457+
host_outdir_tgt # type: Optional[Text]
458+
): # type: (...) -> None
459+
"""Append volume a file/dir mapping to the runtime option list."""
460+
pass
461+
462+
@abstractmethod
463+
def add_writable_file_volume(self,
464+
runtime, # type: List[Text]
465+
volume, # type: MapperEnt
466+
host_outdir_tgt # type: Optional[Text]
467+
): # type: (...) -> None
468+
"""Append a writable file mapping to the runtime option list."""
469+
pass
470+
471+
@abstractmethod
472+
def add_writable_directory_volume(self,
473+
runtime, # type: List[Text]
474+
volume, # type: MapperEnt
475+
host_outdir_tgt # type: Optional[Text]
476+
): # type: (...) -> None
477+
"""Append a writable directory mapping to the runtime option list."""
478+
pass
479+
480+
def create_file_and_add_volume(self,
481+
runtime, # type: List[Text]
482+
volume, # type: MapperEnt
483+
host_outdir_tgt, # type: Optional[Text]
484+
secret_store # type: Optional[SecretStore]
485+
): # type: (...) -> None
486+
"""Create the file and add a mapping."""
487+
if not host_outdir_tgt:
488+
new_file = os.path.join(
489+
tempfile.mkdtemp(dir=self.tmpdir),
490+
os.path.basename(volume.resolved))
491+
writable = True if volume.type == "CreateWritableFile" else False
492+
if secret_store:
493+
contents = secret_store.retrieve(volume.resolved)
494+
else:
495+
contents = volume.resolved
496+
dirname = os.path.dirname(host_outdir_tgt or new_file)
497+
if not os.path.exists(dirname):
498+
os.makedirs(dirname, 0o0755)
499+
with open(host_outdir_tgt or new_file, "wb") as file_literal:
500+
file_literal.write(contents.encode("utf-8"))
501+
if not host_outdir_tgt:
502+
self.append_volume(runtime, new_file, volume.target,
503+
writable=writable)
504+
if writable:
505+
ensure_writable(host_outdir_tgt or new_file)
506+
else:
507+
ensure_non_writable(host_outdir_tgt or new_file)
508+
509+
510+
511+
def add_volumes(self,
512+
pathmapper, # type: PathMapper
513+
runtime, # type: List[Text]
514+
secret_store=None, # type: Optional[SecretStore]
515+
any_path_okay=False # type: bool
516+
): # type: (...) -> None
517+
"""Append volume mappings to the runtime option list."""
518+
519+
container_outdir = self.builder.outdir
520+
for vol in (itm[1] for itm in pathmapper.items() if itm[1].staged):
521+
host_outdir_tgt = None # type: Optional[Text]
522+
if vol.target.startswith(container_outdir + "/"):
523+
host_outdir_tgt = os.path.join(
524+
self.outdir, vol.target[len(container_outdir)+1:])
525+
if not host_outdir_tgt and not any_path_okay:
526+
raise WorkflowException(
527+
"No mandatory DockerRequirement, yet path is outside "
528+
"the designated output directory, also know as "
529+
"$(runtime.outdir): {}".format(vol))
530+
if vol.type in ("File", "Directory"):
531+
self.add_file_or_directory_volume(
532+
runtime, vol, host_outdir_tgt)
533+
elif vol.type == "WritableFile":
534+
self.add_writable_file_volume(runtime, vol, host_outdir_tgt)
535+
elif vol.type == "WritableDirectory":
536+
self.add_writable_directory_volume(
537+
runtime, vol, host_outdir_tgt)
538+
elif vol.type in ["CreateFile", "CreateWritableFile"]:
539+
self.create_file_and_add_volume(
540+
runtime, vol, host_outdir_tgt, secret_store)
541+
430542
def run(self, runtimeContext):
431543
# type: (RuntimeContext) -> None
432544
if not os.path.exists(self.tmpdir):

0 commit comments

Comments
 (0)