22
33import copy
44import hashlib
5+ import json
56import logging
67import os
78import os .path
1011import sys
1112import threading
1213from collections .abc import Callable , MutableMapping , MutableSequence
13- from subprocess import check_call , check_output # nosec
14+ from subprocess import check_call , check_output , run # nosec
1415from typing import cast
1516
1617from cwl_utils .types import CWLDirectoryType , CWLFileType , CWLObjectType
18+ from mypy_extensions import mypyc_attr
1719from packaging .version import Version
1820from schema_salad .sourceline import SourceLine
1921from schema_salad .utils import json_dumps
@@ -165,6 +167,30 @@ def _normalize_sif_id(string: str) -> str:
165167 return string .replace ("/" , "_" ) + ".sif"
166168
167169
170+ @mypyc_attr (allow_interpreted_subclasses = True )
171+ def _inspect_singularity_sandbox_image (path : str ) -> bool :
172+ """Inspect singularity sandbox image to be sure it is not an empty directory."""
173+ cmd = [
174+ "singularity" ,
175+ "inspect" ,
176+ "--json" ,
177+ path ,
178+ ]
179+ try :
180+ result = run (cmd , capture_output = True , text = True ) # nosec
181+ except Exception :
182+ return False
183+
184+ if result .returncode == 0 :
185+ try :
186+ output = json .loads (result .stdout )
187+ except json .JSONDecodeError :
188+ return False
189+ if output .get ("data" , {}).get ("attributes" , {}):
190+ return True
191+ return False
192+
193+
168194class SingularityCommandLineJob (ContainerCommandLineJob ):
169195 def __init__ (
170196 self ,
@@ -186,6 +212,7 @@ def get_image(
186212 pull_image : bool ,
187213 tmp_outdir_prefix : str ,
188214 force_pull : bool = False ,
215+ sandbox_base_path : str | None = None ,
189216 ) -> bool :
190217 """
191218 Acquire the software container image in the specified dockerRequirement.
@@ -204,17 +231,34 @@ def get_image(
204231
205232 with _IMAGES_LOCK :
206233 if "dockerImageId" in dockerRequirement :
207- if (d_image_id := dockerRequirement ["dockerImageId" ]) in _IMAGES :
234+ d_image_id = dockerRequirement ["dockerImageId" ]
235+ if d_image_id in _IMAGES :
208236 if (resolved_image_id := _IMAGES [d_image_id ]) != d_image_id :
209237 dockerRequirement ["dockerImage_id" ] = resolved_image_id
210238 return True
239+ if d_image_id .startswith ("/" ):
240+ _logger .info (
241+ SourceLine (dockerRequirement , "dockerImageId" ).makeError (
242+ f"Non-portable: using an absolute file path in a 'dockerImageId': { d_image_id } "
243+ )
244+ )
211245
212246 docker_req = copy .deepcopy (dockerRequirement ) # thread safety
213247 if "CWL_SINGULARITY_CACHE" in os .environ :
214248 cache_folder = os .environ ["CWL_SINGULARITY_CACHE" ]
215249 elif is_version_2_6 () and "SINGULARITY_PULLFOLDER" in os .environ :
216250 cache_folder = os .environ ["SINGULARITY_PULLFOLDER" ]
217251
252+ if os .environ .get ("CWL_SINGULARITY_IMAGES" , None ):
253+ image_base_path = os .environ ["CWL_SINGULARITY_IMAGES" ]
254+ else :
255+ image_base_path = cache_folder if cache_folder else ""
256+
257+ if not sandbox_base_path :
258+ sandbox_base_path = os .path .abspath (image_base_path )
259+ else :
260+ sandbox_base_path = os .path .abspath (sandbox_base_path )
261+
218262 if "dockerFile" in docker_req :
219263 if cache_folder is None : # if environment variables were not set
220264 cache_folder = create_tmp_dir (tmp_outdir_prefix )
@@ -264,21 +308,44 @@ def get_image(
264308 )
265309 found = True
266310 elif "dockerImageId" not in docker_req and "dockerPull" in docker_req :
267- match = re .search (pattern = r"([a-z]*://)" , string = docker_req ["dockerPull" ])
268- img_name = _normalize_image_id (docker_req ["dockerPull" ])
269- candidates .append (img_name )
270- if is_version_3_or_newer ():
271- sif_name = _normalize_sif_id (docker_req ["dockerPull" ])
272- candidates .append (sif_name )
273- docker_req ["dockerImageId" ] = sif_name
311+ # looking for local singularity sandbox image and handle it as a local image
312+ sandbox_image_path = os .path .join (sandbox_base_path , dockerRequirement ["dockerPull" ])
313+ if os .path .isdir (sandbox_image_path ) and _inspect_singularity_sandbox_image (
314+ sandbox_image_path
315+ ):
316+ docker_req ["dockerImageId" ] = sandbox_image_path
317+ _logger .info (
318+ "Using local Singularity sandbox image found in %s" ,
319+ sandbox_image_path ,
320+ )
321+ found = True
274322 else :
275- docker_req ["dockerImageId" ] = img_name
276- if not match :
277- docker_req ["dockerPull" ] = "docker://" + docker_req ["dockerPull" ]
323+ match = re .search (pattern = r"([a-z]*://)" , string = docker_req ["dockerPull" ])
324+ img_name = _normalize_image_id (docker_req ["dockerPull" ])
325+ candidates .append (img_name )
326+ if is_version_3_or_newer ():
327+ sif_name = _normalize_sif_id (docker_req ["dockerPull" ])
328+ candidates .append (sif_name )
329+ docker_req ["dockerImageId" ] = sif_name
330+ else :
331+ docker_req ["dockerImageId" ] = img_name
332+ if not match :
333+ docker_req ["dockerPull" ] = "docker://" + docker_req ["dockerPull" ]
278334 elif "dockerImageId" in docker_req :
279- if os .path .isfile (docker_req ["dockerImageId" ]):
335+ sandbox_image_path = os .path .join (sandbox_base_path , dockerRequirement ["dockerImageId" ])
336+ # handling local singularity sandbox image
337+ if os .path .isdir (sandbox_image_path ) and _inspect_singularity_sandbox_image (
338+ sandbox_image_path
339+ ):
340+ _logger .info (
341+ "Using local Singularity sandbox image found in %s" ,
342+ sandbox_image_path ,
343+ )
344+ docker_req ["dockerImageId" ] = sandbox_image_path
280345 found = True
281346 else :
347+ if os .path .isfile (docker_req ["dockerImageId" ]):
348+ found = True
282349 candidates .append (docker_req ["dockerImageId" ])
283350 candidates .append (_normalize_image_id (docker_req ["dockerImageId" ]))
284351 if is_version_3_or_newer ():
@@ -297,18 +364,19 @@ def get_image(
297364 path = os .path .join (dirpath , entry )
298365 if os .path .isfile (path ):
299366 _logger .info (
300- "Using local copy of Singularity image found in %s" ,
367+ "Using local copy of Singularity image %s found in %s" ,
368+ entry ,
301369 dirpath ,
302370 )
303371 docker_req ["dockerImageId" ] = path
304372 found = True
305373 if (force_pull or not found ) and pull_image :
306374 cmd : list [str ] = []
307375 if "dockerPull" in docker_req :
308- if cache_folder :
376+ if image_base_path :
309377 env = os .environ .copy ()
310378 if is_version_2_6 ():
311- env ["SINGULARITY_PULLFOLDER" ] = cache_folder
379+ env ["SINGULARITY_PULLFOLDER" ] = image_base_path
312380 cmd = [
313381 "singularity" ,
314382 "pull" ,
@@ -323,14 +391,14 @@ def get_image(
323391 "pull" ,
324392 "--force" ,
325393 "--name" ,
326- "{}/{}" .format (cache_folder , docker_req ["dockerImageId" ]),
394+ "{}/{}" .format (image_base_path , docker_req ["dockerImageId" ]),
327395 str (docker_req ["dockerPull" ]),
328396 ]
329397
330398 _logger .info (str (cmd ))
331399 check_call (cmd , env = env , stdout = sys .stderr ) # nosec
332400 docker_req ["dockerImageId" ] = "{}/{}" .format (
333- cache_folder , docker_req ["dockerImageId" ]
401+ image_base_path , docker_req ["dockerImageId" ]
334402 )
335403 found = True
336404 else :
@@ -388,6 +456,7 @@ def get_from_requirements(
388456 pull_image : bool ,
389457 force_pull : bool ,
390458 tmp_outdir_prefix : str ,
459+ image_base_path : str | None = None ,
391460 ) -> str | None :
392461 """
393462 Return the filename of the Singularity image.
@@ -397,8 +466,14 @@ def get_from_requirements(
397466 if not bool (shutil .which ("singularity" )):
398467 raise WorkflowException ("singularity executable is not available" )
399468
400- if not self .get_image (cast (dict [str , str ], r ), pull_image , tmp_outdir_prefix , force_pull ):
401- raise WorkflowException ("Container image {} not found" .format (r ["dockerImageId" ]))
469+ if not self .get_image (
470+ cast (dict [str , str ], r ),
471+ pull_image ,
472+ tmp_outdir_prefix ,
473+ force_pull ,
474+ sandbox_base_path = image_base_path ,
475+ ):
476+ raise WorkflowException (f"Container image not found for { r } " )
402477
403478 return os .path .abspath (cast (str , r ["dockerImageId" ]))
404479
0 commit comments