Skip to content

Commit 88a1aee

Browse files
authored
Merge pull request #715 from common-workflow-language/singularity_tighten
Tighten our use of Singularity
2 parents 1e9b9c1 + d21934d commit 88a1aee

File tree

5 files changed

+119
-62
lines changed

5 files changed

+119
-62
lines changed

cwltool/resolver.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
from __future__ import absolute_import
22
import logging
33
import os
4+
import sys
45

6+
if sys.version_info < (3, 4):
7+
from pathlib2 import Path
8+
else:
9+
from pathlib import Path
510
from six.moves import urllib
611

7-
from schema_salad.ref_resolver import file_uri
812

913
_logger = logging.getLogger("cwltool")
1014

1115

1216
def resolve_local(document_loader, uri):
13-
if uri.startswith("/"):
14-
return None
15-
shares = [os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser('~'), ".local", "share"))]
16-
shares.extend(os.environ.get("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(":"))
17-
shares = [os.path.join(s, "commonwl", uri) for s in shares]
18-
shares.insert(0, os.path.join(os.getcwd(), uri))
17+
if uri.startswith("/") and os.path.exists(uri):
18+
return Path(uri).as_uri()
19+
if os.path.exists(urllib.parse.urlparse(
20+
urllib.parse.urldefrag(
21+
"{}/{}".format(Path.cwd().as_uri(), uri))[0])[2]):
22+
return "{}/{}".format(Path.cwd().as_uri(), uri)
23+
sharepaths = [os.environ.get("XDG_DATA_HOME", os.path.join(
24+
os.path.expanduser('~'), ".local", "share"))]
25+
sharepaths.extend(os.environ.get(
26+
"XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(":"))
27+
shares = [os.path.join(s, "commonwl", uri) for s in sharepaths]
1928

2029
_logger.debug("Search path is %s", shares)
2130

22-
for s in shares:
23-
if os.path.exists(s):
24-
return file_uri(s)
25-
if os.path.exists("%s.cwl" % s):
26-
return file_uri(s)
31+
for path in shares:
32+
if os.path.exists(path):
33+
return Path(uri).as_uri()
34+
if os.path.exists("{}.cwl".format(path)):
35+
return Path("{}.cwl".format(path)).as_uri()
2736
return None
2837

2938

@@ -32,7 +41,6 @@ def tool_resolver(document_loader, uri):
3241
ret = r(document_loader, uri)
3342
if ret is not None:
3443
return ret
35-
return file_uri(os.path.abspath(uri), split_frag=True)
3644

3745

3846
ga4gh_tool_registries = ["https://dockstore.org:8443"]

cwltool/singularity.py

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
from io import open
1010

11-
from typing import (Dict, List, Text, MutableMapping, Any)
11+
from typing import (Dict, List, Text, Optional, MutableMapping, Any)
1212

1313
from .errors import WorkflowException
1414
from .job import ContainerCommandLineJob
@@ -21,34 +21,52 @@
2121

2222
class SingularityCommandLineJob(ContainerCommandLineJob):
2323
@staticmethod
24-
def get_image(dockerRequirement, pull_image, dry_run=False):
25-
# type: (Dict[Text, Text], bool, bool) -> bool
24+
def get_image(dockerRequirement, # type: Dict[Text, Text]
25+
pull_image, # type: bool
26+
dry_run=False # type: bool
27+
):
28+
# type: (...) -> bool
29+
"""
30+
Acquire the software container image in the specified dockerRequirement
31+
using Singularity and returns the success as a bool. Updates the
32+
provided dockerRequirement with the specific dockerImageId to the full
33+
path of the local image, if found. Likewise the
34+
dockerRequirement['dockerPull'] is updated to a docker:// URI if needed.
35+
"""
2636
found = False
2737

2838
if "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
2939
match = re.search(pattern=r'([a-z]*://)', string=dockerRequirement["dockerPull"])
3040
if match:
3141
dockerRequirement["dockerImageId"] = re.sub(pattern=r'([a-z]*://)', repl=r'',
3242
string=dockerRequirement["dockerPull"])
33-
dockerRequirement["dockerImageId"] = re.sub(pattern=r'[:/]', repl=r'-',
34-
string=dockerRequirement["dockerImageId"]) + ".img"
43+
dockerRequirement["dockerImageId"] = re.sub(
44+
pattern=r'[:/]', repl=r'-', string=dockerRequirement["dockerImageId"]) + ".img"
3545
else:
36-
dockerRequirement["dockerImageId"] = re.sub(pattern=r'[:/]', repl=r'-',
37-
string=dockerRequirement["dockerPull"]) + ".img"
46+
dockerRequirement["dockerImageId"] = re.sub(
47+
pattern=r'[:/]', repl=r'-', string=dockerRequirement["dockerPull"]) + ".img"
3848
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
3949

4050
# check if Singularity image is available in $SINGULARITY_CACHEDIR
41-
if "SINGULARITY_CACHEDIR" in os.environ \
42-
and os.path.isfile(os.path.join(os.environ["SINGULARITY_CACHEDIR"], dockerRequirement["dockerImageId"])):
43-
_logger.info("Using local copy of Singularity image found in $SINGULARITY_CACHEDIR")
44-
dockerRequirement["dockerImageId"] = os.path.join(os.environ["SINGULARITY_CACHEDIR"], dockerRequirement["dockerImageId"])
51+
if "SINGULARITY_CACHEDIR" in os.environ and os.path.isfile(
52+
os.path.join(os.environ["SINGULARITY_CACHEDIR"],
53+
dockerRequirement["dockerImageId"])):
54+
_logger.info("Using local copy of Singularity image found in "
55+
"$SINGULARITY_CACHEDIR")
56+
dockerRequirement["dockerImageId"] = os.path.join(
57+
os.environ["SINGULARITY_CACHEDIR"],
58+
dockerRequirement["dockerImageId"])
4559
found = True
4660

4761
# check if Singularity image is available in $SINGULARITY_PULLFOLDER
48-
elif "SINGULARITY_PULLFOLDER" in os.environ \
49-
and os.path.isfile(os.path.join(os.environ["SINGULARITY_PULLFOLDER"], dockerRequirement["dockerImageId"])):
50-
_logger.info("Using local copy of Singularity image found in $SINGULARITY_PULLFOLDER")
51-
dockerRequirement["dockerImageId"] = os.path.join(os.environ["SINGULARITY_PULLFOLDER"], dockerRequirement["dockerImageId"])
62+
elif "SINGULARITY_PULLFOLDER" in os.environ and os.path.isfile(
63+
os.path.join(os.environ["SINGULARITY_PULLFOLDER"],
64+
dockerRequirement["dockerImageId"])):
65+
_logger.info("Using local copy of Singularity image found in "
66+
"$SINGULARITY_PULLFOLDER")
67+
dockerRequirement["dockerImageId"] = os.path.join(
68+
os.environ["SINGULARITY_PULLFOLDER"],
69+
dockerRequirement["dockerImageId"])
5270
found = True
5371

5472
# check if Singularity image is available in current working directory
@@ -69,21 +87,31 @@ def get_image(dockerRequirement, pull_image, dry_run=False):
6987

7088
return found
7189

72-
def get_from_requirements(self, r, req, pull_image, dry_run=False, force_pull=False):
73-
# type: (Dict[Text, Text], bool, bool, bool, bool) -> Text
74-
# returns the filename of the Singularity image (e.g. hello-world-latest.img)
90+
def get_from_requirements(self,
91+
r, # type: Optional[Dict[Text, Text]]
92+
req, # type: bool
93+
pull_image, # type: bool
94+
dry_run=False, # type: bool
95+
force_pull=False # type: bool
96+
):
97+
# type: (...) -> Text
98+
"""
99+
Returns the filename of the Singularity image (e.g.
100+
hello-world-latest.img).
101+
"""
75102

76103
if force_pull:
77-
_logger.warn("--force-docker-pull currently not supported for singularity")
104+
_logger.warning("--force-docker-pull currently not supported for "
105+
"singularity")
78106

79107
if r:
80108
errmsg = None
81109
try:
82110
subprocess.check_output(["singularity", "--version"])
83-
except subprocess.CalledProcessError as e:
84-
errmsg = "Cannot execute 'singularity --version' " + Text(e)
85-
except OSError as e:
86-
errmsg = "'singularity' executable not found: " + Text(e)
111+
except subprocess.CalledProcessError as err:
112+
errmsg = "Cannot execute 'singularity --version' {}".format(err)
113+
except OSError as err:
114+
errmsg = "'singularity' executable not found: {}".format(err)
87115

88116
if errmsg:
89117
if req:
@@ -95,7 +123,8 @@ def get_from_requirements(self, r, req, pull_image, dry_run=False, force_pull=Fa
95123
return os.path.abspath(r["dockerImageId"])
96124
else:
97125
if req:
98-
raise WorkflowException(u"Container image %s not found" % r["dockerImageId"])
126+
raise WorkflowException(u"Container image {} not "
127+
"found".format(r["dockerImageId"]))
99128

100129
return None
101130

@@ -104,10 +133,10 @@ def add_volumes(self, pathmapper, runtime, stage_output):
104133

105134
host_outdir = self.outdir
106135
container_outdir = self.builder.outdir
107-
for src, vol in pathmapper.items():
136+
for _, vol in pathmapper.items():
108137
if not vol.staged:
109138
continue
110-
if stage_output:
139+
if stage_output and not vol.target.startswith(container_outdir):
111140
containertgt = container_outdir + vol.target[len(host_outdir):]
112141
else:
113142
containertgt = vol.target
@@ -119,13 +148,15 @@ def add_volumes(self, pathmapper, runtime, stage_output):
119148
if vol.type in ("File", "Directory"):
120149
if not vol.resolved.startswith("_:"):
121150
runtime.append(u"--bind")
122-
runtime.append("%s:%s:ro" % (
123-
docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
151+
runtime.append("{}:{}:ro".format(
152+
docker_windows_path_adjust(vol.resolved),
153+
docker_windows_path_adjust(containertgt)))
124154
elif vol.type == "WritableFile":
125155
if self.inplace_update:
126156
runtime.append(u"--bind")
127-
runtime.append("%s:%s:rw" % (
128-
docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
157+
runtime.append("{}:{}:rw".format(
158+
docker_windows_path_adjust(vol.resolved),
159+
docker_windows_path_adjust(containertgt)))
129160
else:
130161
shutil.copy(vol.resolved, host_outdir_tgt)
131162
ensure_writable(host_outdir_tgt)
@@ -135,28 +166,40 @@ def add_volumes(self, pathmapper, runtime, stage_output):
135166
else:
136167
if self.inplace_update:
137168
runtime.append(u"--bind")
138-
runtime.append("%s:%s:rw" % (
139-
docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
169+
runtime.append("{}:{}:rw".format(
170+
docker_windows_path_adjust(vol.resolved),
171+
docker_windows_path_adjust(containertgt)))
140172
else:
141173
shutil.copytree(vol.resolved, vol.target)
142174
elif vol.type == "CreateFile":
143175
createtmp = os.path.join(host_outdir, os.path.basename(vol.target))
144-
with open(createtmp, "wb") as f:
145-
f.write(vol.resolved.encode("utf-8"))
176+
with open(createtmp, "wb") as tmp:
177+
tmp.write(vol.resolved.encode("utf-8"))
146178
runtime.append(u"--bind")
147-
runtime.append(
148-
"%s:%s:ro" % (docker_windows_path_adjust(createtmp), docker_windows_path_adjust(vol.target)))
149-
150-
def create_runtime(self, env, rm_container=True, record_container_id=False, cidfile_dir="",
151-
cidfile_prefix="", **kwargs):
152-
# type: (MutableMapping[Text, Text], bool, bool, Text, Text, **Any) -> List
153-
154-
runtime = [u"singularity", u"--quiet", u"exec"]
179+
runtime.append("{}:{}:ro".format(
180+
docker_windows_path_adjust(createtmp),
181+
docker_windows_path_adjust(vol.target)))
182+
183+
def create_runtime(self,
184+
env, # type: MutableMapping[Text, Text]
185+
rm_container=True, # type: bool
186+
record_container_id=False, # type: bool
187+
cidfile_dir="", # type: Text
188+
cidfile_prefix="", # type: Text
189+
**kwargs
190+
):
191+
# type: (...) -> List
192+
""" Returns the Singularity runtime list of commands and options."""
193+
194+
runtime = [u"singularity", u"--quiet", u"exec", u"--contain", u"--pid",
195+
u"--ipc"] # , u"--userns"]
155196
runtime.append(u"--bind")
156-
runtime.append(
157-
u"%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.outdir)), self.builder.outdir))
197+
runtime.append(u"{}:{}:rw".format(
198+
docker_windows_path_adjust(os.path.realpath(self.outdir)),
199+
self.builder.outdir))
158200
runtime.append(u"--bind")
159-
runtime.append(u"%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
201+
runtime.append(u"{}:{}:rw".format(
202+
docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
160203

161204
self.add_volumes(self.pathmapper, runtime, stage_output=False)
162205
if self.generatemapper:
@@ -167,11 +210,13 @@ def create_runtime(self, env, rm_container=True, record_container_id=False, cidf
167210

168211
if kwargs.get("custom_net", None) is not None:
169212
raise UnsupportedRequirement(
170-
"Singularity implementation does not support networking")
213+
"Singularity implementation does not support custom networking")
214+
elif kwargs.get("disable_net", None):
215+
runtime.append(u"--net")
171216

172217
env["SINGULARITYENV_TMPDIR"] = "/tmp"
173218
env["SINGULARITYENV_HOME"] = self.builder.outdir
174219

175-
for t, v in self.environment.items():
176-
env["SINGULARITYENV_" + t] = v
220+
for name, value in self.environment.items():
221+
env["SINGULARITYENV_{}".format(name)] = value
177222
return runtime

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ rdflib-jsonld==0.4.0
55
shellescape==3.4.1
66
schema-salad>=2.6,<3
77
typing==3.5.3
8+
pathlib2; python_version<"3"

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
'six >= 1.8.0',
5959
],
6060
extras_require={
61+
':python_version<"3"': [ 'pathlib2' ],
6162
'deps': ["galaxy-lib >= 17.09.3"]
6263
},
6364
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4',

tox.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ deps =
4848
[testenv:py27-pipconflictchecker]
4949
commands = pipconflictchecker
5050
whitelist_externals = pipconflictchecker
51-
deps = pip-conflict-checker
51+
deps =
52+
pip-conflict-checker
53+
pip==9.0.3
5254

5355
[testenv:py27-lint-readme]
5456
commands = python setup.py check -r -s

0 commit comments

Comments
 (0)