Skip to content

Commit aa2d695

Browse files
committed
singularity dockerFile support, add tests and fixes
1 parent fd16f40 commit aa2d695

File tree

4 files changed

+160
-25
lines changed

4 files changed

+160
-25
lines changed

cwltool/singularity.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Support for executing Docker format containers using Singularity {2,3}.x or Apptainer 1.x."""
22

33
import copy
4+
import hashlib
45
import logging
56
import os
67
import os.path
@@ -13,6 +14,7 @@
1314
from typing import cast
1415

1516
from schema_salad.sourceline import SourceLine
17+
from schema_salad.utils import json_dumps
1618
from spython.main import Client
1719
from spython.main.parse.parsers.docker import DockerParser
1820
from spython.main.parse.writers.singularity import SingularityWriter
@@ -206,41 +208,46 @@ def get_image(
206208
absolute_path = os.path.abspath(cache_folder)
207209
if "dockerImageId" in docker_req:
208210
image_name = docker_req["dockerImageId"]
209-
image_path = os.path.join(absolute_path, image_name)
210-
if os.path.exists(image_path):
211-
found = True
211+
else:
212+
image_name = hashlib.md5( # nosec
213+
json_dumps(dockerRequirement, separators=(",", ":"), sort_keys=True).encode(
214+
"utf-8"
215+
)
216+
).hexdigest()
217+
if is_version_3_or_newer():
218+
image_name = _normalize_sif_id(image_name)
219+
else:
220+
image_name = _normalize_image_id(image_name)
221+
image_name = os.path.join(absolute_path, image_name)
222+
docker_req["dockerImageId"] = image_name
223+
if os.path.exists(image_name):
224+
found = True
212225
if found is False:
213226
dockerfile_path = os.path.join(absolute_path, "Dockerfile")
214227
singularityfile_path = dockerfile_path + ".def"
215-
# if you do not set APPTAINER_TMPDIR will crash
216-
# WARNING: 'nodev' mount option set on /tmp, it could be a
217-
# source of failure during build process
218-
# FATAL: Unable to create build: 'noexec' mount option set on
219-
# /tmp, temporary root filesystem won't be usable at this location
220228
with open(dockerfile_path, "w") as dfile:
221229
dfile.write(docker_req["dockerFile"])
222230

223-
singularityfile = SingularityWriter(DockerParser(dockerfile_path).parse()).convert()
231+
docker_recipe = DockerParser(dockerfile_path).parse()
232+
docker_recipe["spython-base"].entrypoint = ""
233+
singularityfile = SingularityWriter(docker_recipe).convert()
224234
with open(singularityfile_path, "w") as file:
225235
file.write(singularityfile)
226236

237+
# if you do not set APPTAINER_TMPDIR will crash
238+
# WARNING: 'nodev' mount option set on /tmp, it could be a
239+
# source of failure during build process
240+
# FATAL: Unable to create build: 'noexec' mount option set on
241+
# /tmp, temporary root filesystem won't be usable at this location
227242
os.environ["APPTAINER_TMPDIR"] = absolute_path
228243
singularity_options = ["--fakeroot"] if not shutil.which("proot") else []
229-
if "dockerImageId" in docker_req:
230-
Client.build(
231-
recipe=singularityfile_path,
232-
build_folder=absolute_path,
233-
image=docker_req["dockerImageId"],
234-
sudo=False,
235-
options=singularity_options,
236-
)
237-
else:
238-
Client.build(
239-
recipe=singularityfile_path,
240-
build_folder=absolute_path,
241-
sudo=False,
242-
options=singularity_options,
243-
)
244+
Client.build(
245+
recipe=singularityfile_path,
246+
build_folder=absolute_path,
247+
image=image_name,
248+
sudo=False,
249+
options=singularity_options,
250+
)
244251
found = True
245252
elif "dockerImageId" not in docker_req and "dockerPull" in docker_req:
246253
match = re.search(pattern=r"([a-z]*://)", string=docker_req["dockerPull"])
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env cwl-runner
2+
cwlVersion: v1.0
3+
class: CommandLineTool
4+
5+
requirements:
6+
DockerRequirement:
7+
dockerFile: |
8+
FROM docker.io/debian:stable-slim
9+
dockerImageId: customDebian
10+
11+
inputs:
12+
message: string
13+
14+
outputs: []
15+
16+
baseCommand: echo
17+
arguments:
18+
- $(inputs.message)

tests/sing_dockerfile_test.cwl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env cwl-runner
2+
cwlVersion: v1.0
3+
class: CommandLineTool
4+
5+
requirements:
6+
DockerRequirement:
7+
dockerFile: |
8+
FROM docker.io/debian:stable-slim
9+
10+
inputs:
11+
message: string
12+
13+
outputs: []
14+
15+
baseCommand: echo
16+
arguments:
17+
- $(inputs.message)

tests/test_singularity.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from cwltool.main import main
9+
from cwltool.singularity import _IMAGES, _IMAGES_LOCK
910

1011
from .util import (
1112
get_data,
@@ -17,6 +18,12 @@
1718
)
1819

1920

21+
@pytest.fixture(autouse=True)
22+
def clear_singularity_image_cache() -> None:
23+
with _IMAGES_LOCK:
24+
_IMAGES.clear()
25+
26+
2027
@needs_singularity_2_6
2128
def test_singularity_pullfolder(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
2229
"""Test singularity respects SINGULARITY_PULLFOLDER."""
@@ -149,7 +156,7 @@ def test_singularity3_docker_image_id_in_tool(tmp_path: Path) -> None:
149156
"hello",
150157
]
151158
)
152-
assert result_code == 0
159+
assert result_code == 0, stderr
153160
result_code1, stdout, stderr = get_main_output(
154161
[
155162
"--singularity",
@@ -159,3 +166,89 @@ def test_singularity3_docker_image_id_in_tool(tmp_path: Path) -> None:
159166
]
160167
)
161168
assert result_code1 == 0
169+
170+
171+
@needs_singularity_3_or_newer
172+
def test_singularity_dockerfile_no_name_no_cache(tmp_path: Path) -> None:
173+
workdir = tmp_path / "working_dir"
174+
workdir.mkdir()
175+
with working_directory(workdir):
176+
result_code, stdout, stderr = get_main_output(
177+
[
178+
"--singularity",
179+
get_data("tests/sing_dockerfile_test.cwl"),
180+
"--message",
181+
"hello",
182+
]
183+
)
184+
assert result_code == 0, stderr
185+
assert not (workdir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
186+
187+
188+
@needs_singularity_3_or_newer
189+
def test_singularity_dockerfile_no_name_with_cache(
190+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
191+
) -> None:
192+
workdir = tmp_path / "working_dir"
193+
workdir.mkdir()
194+
cachedir = tmp_path / "cache"
195+
cachedir.mkdir()
196+
monkeypatch.setenv("CWL_SINGULARITY_CACHE", str(cachedir))
197+
with working_directory(workdir):
198+
result_code, stdout, stderr = get_main_output(
199+
[
200+
"--singularity",
201+
get_data("tests/sing_dockerfile_test.cwl"),
202+
"--message",
203+
"hello",
204+
]
205+
)
206+
assert result_code == 0, stderr
207+
assert not (workdir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
208+
assert (cachedir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
209+
210+
211+
@needs_singularity_3_or_newer
212+
def test_singularity_dockerfile_with_name_no_cache(tmp_path: Path) -> None:
213+
workdir = tmp_path / "working_dir"
214+
workdir.mkdir()
215+
with working_directory(workdir):
216+
result_code, stdout, stderr = get_main_output(
217+
[
218+
"--singularity",
219+
get_data("tests/sing_dockerfile_named_test.cwl"),
220+
"--message",
221+
"hello",
222+
]
223+
)
224+
assert result_code == 0, stderr
225+
print(list(workdir.iterdir()))
226+
assert not (workdir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
227+
assert not (workdir / "customDebian_latest.sif").exists()
228+
229+
230+
@needs_singularity_3_or_newer
231+
def test_singularity_dockerfile_with_name_with_cache(
232+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
233+
) -> None:
234+
workdir = tmp_path / "working_dir"
235+
workdir.mkdir()
236+
cachedir = tmp_path / "cache"
237+
cachedir.mkdir()
238+
monkeypatch.setenv("CWL_SINGULARITY_CACHE", str(cachedir))
239+
with working_directory(workdir):
240+
result_code, stdout, stderr = get_main_output(
241+
[
242+
"--singularity",
243+
get_data("tests/sing_dockerfile_named_test.cwl"),
244+
"--message",
245+
"hello",
246+
]
247+
)
248+
print(list(workdir.iterdir()))
249+
print(list(cachedir.iterdir()))
250+
assert result_code == 0, stderr
251+
assert not (workdir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
252+
assert not (cachedir / "bea92b9b6910cbbd2ae602f5bb0f0f27_latest.sif").exists()
253+
assert not (workdir / "customDebian_latest.sif").exists()
254+
assert (cachedir / "customDebian_latest.sif").exists()

0 commit comments

Comments
 (0)