Skip to content

Commit 6b4c430

Browse files
committed
fixes from conformance tests and varying Singularity version
1 parent b02fcaf commit 6b4c430

File tree

4 files changed

+110
-41
lines changed

4 files changed

+110
-41
lines changed

README.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,14 @@ or
138138
139139
cwltool --user-space-docker-cmd=dx-docker https://raw.githubusercontent.com/common-workflow-language/common-workflow-language/master/v1.0/v1.0/test-cwl-out2.cwl https://github.com/common-workflow-language/common-workflow-language/blob/master/v1.0/v1.0/empty.json
140140
141-
``cwltool`` can use `Singularity <http://singularity.lbl.gov/>`_ as a Docker container runtime, an experimental feature.
142-
Singularity will run software containers specified in ``DockerRequirement`` and therefore works with Docker images only,
143-
native Singularity images are not supported.
144-
To use Singularity as the Docker container runtime, provide ``--singularity`` command line option to ``cwltool``.
141+
``cwltool`` can use `Singularity <http://singularity.lbl.gov/>`_ version 2.6.1
142+
or later as a Docker container runtime.
143+
``cwltool`` with Singularity will run software containers specified in
144+
``DockerRequirement`` and therefore works with Docker images only, native
145+
Singularity images are not supported. To use Singularity as the Docker container
146+
runtime, provide ``--singularity`` command line option to ``cwltool``.
147+
With Singularity, ``cwltool`` can pass all CWL v1.0 conformance tests, except
148+
those involving Docker container ENTRYPOINTs.
145149

146150

147151
.. code:: bash

cwltool/singularity.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
pass
3939

4040
_USERNS = None
41+
_SINGULARITY_VERSION = None
4142

4243
def _singularity_supports_userns(): # type: ()->bool
4344
global _USERNS # pylint: disable=global-statement
@@ -53,6 +54,25 @@ def _singularity_supports_userns(): # type: ()->bool
5354
_USERNS = False
5455
return _USERNS
5556

57+
58+
def get_version():
59+
global _SINGULARITY_VERSION # pylint: disable=global-statement
60+
if not _SINGULARITY_VERSION:
61+
_SINGULARITY_VERSION = check_output(["singularity", "--version"], text=True)
62+
if _SINGULARITY_VERSION.startswith("singularity version "):
63+
_SINGULARITY_VERSION = _SINGULARITY_VERSION[20:]
64+
return _SINGULARITY_VERSION
65+
66+
def is_version_2_6():
67+
return get_version().startswith("2.6")
68+
69+
def is_version_3_or_newer():
70+
return int(get_version()[0]) >= 3
71+
72+
def is_version_3_1_or_newer(): # OCI compatible ??
73+
version = get_version().split('.')
74+
return int(version[0]) >= 4 or (int(version[0]) == 3 and int(version[1]) >= 1)
75+
5676
def _normalize_image_id(string): # type: (Text)->Text
5777
return string.replace('/', '_') + '.img'
5878

@@ -78,30 +98,40 @@ def get_image(dockerRequirement, # type: Dict[Text, Text]
7898

7999
candidates = []
80100

101+
cache_folder = None
102+
if "CWL_SINGULARITY_CACHE" in os.environ:
103+
cache_folder = os.environ["CWL_SINGULARITY_CACHE"]
104+
elif is_version_2_6() and "SINGULARITY_PULLFOLDER" in os.environ:
105+
cache_folder = os.environ["SINGULARITY_PULLFOLDER"]
106+
81107
if "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
82108
match = re.search(pattern=r'([a-z]*://)', string=dockerRequirement["dockerPull"])
83-
candidate_image = _normalize_image_id(dockerRequirement['dockerPull'])
84-
candidates.append(candidate_image)
85-
candidate_sif = _normalize_sif_id(dockerRequirement['dockerPull'])
86-
candidates.append(candidate_sif)
87-
dockerRequirement['dockerImageId'] = candidate_image
109+
img_name = _normalize_image_id(dockerRequirement['dockerPull'])
110+
candidates.append(img_name)
111+
if is_version_3_or_newer():
112+
sif_name = _normalize_sif_id(dockerRequirement['dockerPull'])
113+
candidates.append(sif_name)
114+
dockerRequirement["dockerImageId"] = sif_name
115+
else:
116+
dockerRequirement["dockerImageId"] = img_name
88117
if not match:
89118
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
90119
elif "dockerImageId" in dockerRequirement:
91120
candidates.append(dockerRequirement['dockerImageId'])
92121
candidates.append(_normalize_image_id(dockerRequirement['dockerImageId']))
122+
if is_version_3_or_newer():
123+
candidates.append(_normalize_sif_id(dockerRequirement['dockerPull']))
93124

94-
# check if Singularity image is available in $SINGULARITY_CACHEDIR
95-
# or any subdirs created in cachedir
96125
targets = [os.getcwd()]
97-
for env in ("SINGULARITY_CACHEDIR", "SINGULARITY_PULLFOLDER"):
98-
if env in os.environ:
99-
targets.append(os.environ[env])
126+
if "CWL_SINGULARITY_CACHE" in os.environ:
127+
targets.append(os.environ["CWL_SINGULARITY_CACHE"])
128+
if is_version_2_6() and "SINGULARITY_PULLFOLDER" in os.environ:
129+
targets.append(os.environ["SINGULARITY_PULLFOLDER"])
100130
for target in targets:
101131
for dirpath, subdirs, files in os.walk(target):
102-
for sif in files:
103-
if sif in candidates:
104-
path = os.path.join(dirpath, sif)
132+
for entry in files:
133+
if entry in candidates:
134+
path = os.path.join(dirpath, entry)
105135
if os.path.isfile(path):
106136
_logger.info(
107137
"Using local copy of Singularity image found in %s",
@@ -111,15 +141,24 @@ def get_image(dockerRequirement, # type: Dict[Text, Text]
111141
if (force_pull or not found) and pull_image:
112142
cmd = [] # type: List[Text]
113143
if "dockerPull" in dockerRequirement:
114-
if "SINGULARITY_PULLFOLDER" in os.environ:
115-
pull_folder = str(os.environ["SINGULARITY_PULLFOLDER"])
116-
cmd = ["singularity", "pull", "--force", "--name",
117-
str('%s/' %pull_folder) + str(dockerRequirement["dockerImageId"]),
118-
str(dockerRequirement["dockerPull"])]
144+
if cache_folder:
145+
env = os.environ.copy()
146+
if is_version_2_6():
147+
env['SINGULARITY_PULLFOLDER'] = cache_folder
148+
cmd = ["singularity", "pull", "--force", "--name",
149+
dockerRequirement["dockerImageId"],
150+
str(dockerRequirement["dockerPull"])]
151+
else:
152+
cmd = ["singularity", "pull", "--force", "--name",
153+
"{}/{}".format(
154+
cache_folder,
155+
dockerRequirement["dockerImageId"]),
156+
str(dockerRequirement["dockerPull"])]
157+
119158
_logger.info(Text(cmd))
120-
check_call(cmd, stdout=sys.stderr) # nosec
121-
dockerRequirement["dockerImageId"] = pull_folder + '/' + \
122-
str(dockerRequirement["dockerImageId"])
159+
check_call(cmd, env=env, stdout=sys.stderr) # nosec
160+
dockerRequirement["dockerImageId"] = '{}/{}'.format(
161+
cache_folder, dockerRequirement["dockerImageId"])
123162
found = True
124163
else:
125164
cmd = ["singularity", "pull", "--force", "--name",
@@ -135,10 +174,21 @@ def get_image(dockerRequirement, # type: Dict[Text, Text]
135174
"dockerFile is not currently supported when using the "
136175
"Singularity runtime for Docker containers."))
137176
elif "dockerLoad" in dockerRequirement:
177+
if is_version_3_1_or_newer():
178+
if 'dockerImageId' in dockerRequirement:
179+
name = "{}.sif".format(dockerRequirement["dockerImageId"])
180+
else:
181+
name = "{}.sif".format(dockerRequirement["dockerLoad"])
182+
cmd ["singularity", "build", name,
183+
"docker-archive://{}".format(dockerRequirement["dockerLoad"])]
184+
_logger.info(Text(cmd))
185+
check_call(cmd, stdout=sys.stderr) # nosec
186+
found = True
187+
dockerRequirement['dockerImageId'] = name
138188
raise WorkflowException(SourceLine(
139189
dockerRequirement, 'dockerLoad').makeError(
140190
"dockerLoad is not currently supported when using the "
141-
"Singularity runtime for Docker containers."))
191+
"Singularity runtime (version less than 3.1) for Docker containers."))
142192
elif "dockerImport" in dockerRequirement:
143193
raise WorkflowException(SourceLine(
144194
dockerRequirement, 'dockerImport').makeError(
@@ -166,10 +216,7 @@ def get_from_requirements(self,
166216
raise WorkflowException(u"Container image {} not "
167217
"found".format(r["dockerImageId"]))
168218

169-
if "SINGULARITY_PULLFOLDER" in os.environ:
170-
return str(r["dockerImageId"])
171-
else:
172-
return os.path.abspath(r["dockerImageId"])
219+
return os.path.abspath(r["dockerImageId"])
173220

174221
@staticmethod
175222
def append_volume(runtime, source, target, writable=False):
@@ -282,10 +329,16 @@ def create_runtime(self,
282329
u"--ipc"]
283330
if _singularity_supports_userns():
284331
runtime.append(u"--userns")
285-
runtime.append(u"--bind")
286-
runtime.append(u"{}:{}:rw".format(
287-
docker_windows_path_adjust(os.path.realpath(self.outdir)),
288-
self.builder.outdir))
332+
if is_version_3_1_or_newer():
333+
runtime.append(u"--home")
334+
runtime.append(u"{}:{}".format(
335+
docker_windows_path_adjust(os.path.realpath(self.outdir)),
336+
self.builder.outdir))
337+
else:
338+
runtime.append(u"--bind")
339+
runtime.append(u"{}:{}:rw".format(
340+
docker_windows_path_adjust(os.path.realpath(self.outdir)),
341+
self.builder.outdir))
289342
runtime.append(u"--bind")
290343
tmpdir = "/tmp" # nosec
291344
runtime.append(u"{}:{}:rw".format(

tests/test_singularity.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
from cwltool.main import main
1010

1111
from .util import (get_data, get_main_output, needs_singularity,
12-
working_directory)
12+
needs_singularity_2_6, working_directory)
1313

1414
sys.argv = ['']
1515

1616

17-
@needs_singularity
17+
@needs_singularity_2_6
1818
def test_singularity_pullfolder(tmp_path):
1919
workdir = tmp_path / "working_dir_new"
2020
workdir.mkdir()
@@ -25,8 +25,11 @@ def test_singularity_pullfolder(tmp_path):
2525
env["SINGULARITY_PULLFOLDER"] = str(pullfolder)
2626
result_code, stdout, stderr = get_main_output(
2727
['--singularity', get_data("tests/sing_pullfolder_test.cwl"), "--message", "hello"], env=env)
28+
print(stdout)
29+
print(stderr)
2830
assert result_code == 0
29-
# TODO, confirm that the image is in the pullfolder
31+
image = pullfolder/"debian.img"
32+
assert image.exists()
3033

3134
@needs_singularity
3235
def test_singularity_workflow(tmpdir):

tests/util.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from cwltool.factory import Factory
1919
from cwltool.resolver import Path
2020
from cwltool.utils import onWindows, subprocess, windows_default_container_id
21-
21+
from cwltool.singularity import is_version_2_6, is_version_3_or_newer
2222

2323

2424
def get_windows_safe_factory(runtime_context=None, # type: RuntimeContext
@@ -55,9 +55,18 @@ def get_data(filename):
5555
reason="Requires the docker executable on the "
5656
"system path.")
5757

58-
needs_singularity = pytest.mark.skipif(not bool(distutils.spawn.find_executable('singularity')),
59-
reason="Requires the singularity executable on the "
60-
"system path.")
58+
needs_singularity = pytest.mark.skipif(
59+
not bool(distutils.spawn.find_executable('singularity')),
60+
reason="Requires the singularity executable on the system path.")
61+
62+
needs_singularity_2_6 = pytest.mark.skipif(
63+
not is_version_2_6(),
64+
reason="Requires that version 2.6.x of singularity executable version is on the system path.")
65+
66+
needs_singularity_3_or_newer = pytest.mark.skipif(
67+
not is_version_3_or_newer,
68+
reason="Requires that version 2.6.x of singularity executable version is on the system path.")
69+
6170

6271
windows_needs_docker = pytest.mark.skipif(
6372
onWindows() and not bool(distutils.spawn.find_executable('docker')),

0 commit comments

Comments
 (0)