Skip to content

Commit dba9616

Browse files
feat: handle local singularity sandbox image
feat: handle local singularity sandbox image fix: skip searching if singularity inspect found image improve singularity inspect test: add test for singularity sandbox image remove unused normalization add comment
1 parent 766d58b commit dba9616

File tree

3 files changed

+99
-15
lines changed

3 files changed

+99
-15
lines changed

cwltool/singularity.py

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

3+
import json
34
import logging
45
import os
56
import os.path
67
import re
78
import shutil
9+
import subprocess
810
import sys
911
from collections.abc import Callable, MutableMapping
10-
from subprocess import check_call, check_output # nosec
12+
from subprocess import DEVNULL, check_call, check_output, run # nosec
1113
from typing import cast
1214

1315
from schema_salad.sourceline import SourceLine
@@ -144,6 +146,27 @@ def _normalize_sif_id(string: str) -> str:
144146
string += "_latest"
145147
return string.replace("/", "_") + ".sif"
146148

149+
def _inspect_singularity_image(path: str) -> bool:
150+
"""Inspect singularity image to be sure it is not an empty directory."""
151+
cmd = [
152+
"singularity",
153+
"inspect",
154+
"--json",
155+
path,
156+
]
157+
try:
158+
result = run(cmd, capture_output=True, text=True)
159+
except Exception:
160+
return False
161+
162+
if result.returncode == 0:
163+
try:
164+
output = json.loads(result.stdout)
165+
except json.JSONDecodeError:
166+
return False
167+
if output.get('data', {}).get('attributes', {}):
168+
return True
169+
return False
147170

148171
class SingularityCommandLineJob(ContainerCommandLineJob):
149172
def __init__(
@@ -229,24 +252,40 @@ def get_image(
229252
)
230253
found = True
231254
elif "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
232-
match = re.search(pattern=r"([a-z]*://)", string=dockerRequirement["dockerPull"])
233-
img_name = _normalize_image_id(dockerRequirement["dockerPull"])
234-
candidates.append(img_name)
235-
if is_version_3_or_newer():
236-
sif_name = _normalize_sif_id(dockerRequirement["dockerPull"])
237-
candidates.append(sif_name)
238-
dockerRequirement["dockerImageId"] = sif_name
255+
# looking for local singularity sandbox image and handle it as a local image
256+
if os.path.isdir(dockerRequirement["dockerPull"]) and _inspect_singularity_image(dockerRequirement["dockerPull"]):
257+
dockerRequirement["dockerImageId"] = dockerRequirement["dockerPull"]
258+
_logger.info(
259+
"Using local Singularity sandbox image found in %s",
260+
dockerRequirement["dockerImageId"],
261+
)
262+
return True
239263
else:
240-
dockerRequirement["dockerImageId"] = img_name
241-
if not match:
242-
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
264+
match = re.search(pattern=r"([a-z]*://)", string=dockerRequirement["dockerPull"])
265+
img_name = _normalize_image_id(dockerRequirement["dockerPull"])
266+
candidates.append(img_name)
267+
if is_version_3_or_newer():
268+
sif_name = _normalize_sif_id(dockerRequirement["dockerPull"])
269+
candidates.append(sif_name)
270+
dockerRequirement["dockerImageId"] = sif_name
271+
else:
272+
dockerRequirement["dockerImageId"] = img_name
273+
if not match:
274+
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
243275
elif "dockerImageId" in dockerRequirement:
244276
if os.path.isfile(dockerRequirement["dockerImageId"]):
245277
found = True
246-
candidates.append(dockerRequirement["dockerImageId"])
247-
candidates.append(_normalize_image_id(dockerRequirement["dockerImageId"]))
248-
if is_version_3_or_newer():
249-
candidates.append(_normalize_sif_id(dockerRequirement["dockerImageId"]))
278+
candidates.append(dockerRequirement["dockerImageId"])
279+
candidates.append(_normalize_image_id(dockerRequirement["dockerImageId"]))
280+
if is_version_3_or_newer():
281+
candidates.append(_normalize_sif_id(dockerRequirement["dockerImageId"]))
282+
# handling local singularity sandbox image
283+
elif os.path.isdir(dockerRequirement["dockerImageId"]) and _inspect_singularity_image(dockerRequirement["dockerImageId"]):
284+
_logger.info(
285+
"Using local Singularity sandbox image found in %s",
286+
dockerRequirement["dockerImageId"],
287+
)
288+
return True
250289

251290
targets = [os.getcwd()]
252291
if "CWL_SINGULARITY_CACHE" in os.environ:

tests/sing_local_sandbox_test.cwl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env cwl-runner
2+
cwlVersion: v1.0
3+
class: CommandLineTool
4+
5+
requirements:
6+
DockerRequirement:
7+
dockerPull: container_repo/alpine
8+
9+
inputs:
10+
message: string
11+
12+
outputs: []
13+
14+
baseCommand: echo

tests/test_singularity.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,34 @@ def test_singularity3_docker_image_id_in_tool(tmp_path: Path) -> None:
159159
]
160160
)
161161
assert result_code1 == 0
162+
163+
@needs_singularity
164+
def test_singularity_local_sandbox_image(tmp_path: Path):
165+
import subprocess
166+
workdir = tmp_path / "working_dir"
167+
workdir.mkdir()
168+
with working_directory(workdir):
169+
# build a sandbox image
170+
container_path = Path(f"{workdir}/container_repo/")
171+
container_path.mkdir()
172+
cmd = [
173+
"apptainer",
174+
"build",
175+
"--sandbox",
176+
container_path / "alpine",
177+
"docker://alpine:latest"
178+
]
179+
build = subprocess.run(cmd, capture_output=True, text=True)
180+
if build.returncode == 0:
181+
result_code, stdout, stderr = get_main_output(
182+
[
183+
"--singularity",
184+
"--disable-pull",
185+
get_data("tests/sing_local_sandbox_test.cwl"),
186+
"--message",
187+
"hello",
188+
]
189+
)
190+
assert result_code == 0
191+
else:
192+
pytest.skip(f"Failed to build the Singularity image: {build.stderr}")

0 commit comments

Comments
 (0)