11"""Support for executing Docker format containers using Singularity {2,3}.x or Apptainer 1.x."""
22
3+ import copy
34import logging
45import os
56import os .path
67import re
78import shutil
89import sys
10+ import threading
911from collections .abc import Callable , MutableMapping
1012from subprocess import check_call , check_output # nosec
1113from typing import cast
3537_SINGULARITY_FLAVOR : str = ""
3638
3739
40+ _IMAGES : dict [str , str ] = {}
41+ _IMAGES_LOCK = threading .Lock ()
42+
43+
3844def get_version () -> tuple [list [int ], str ]:
3945 """
4046 Parse the output of 'singularity --version' to determine the flavor and version.
@@ -180,18 +186,26 @@ def get_image(
180186 cache_folder = None
181187 debug = _logger .isEnabledFor (logging .DEBUG )
182188
189+ with _IMAGES_LOCK :
190+ if "dockerImageId" in dockerRequirement :
191+ if (d_image_id := dockerRequirement ["dockerImageId" ]) in _IMAGES :
192+ if (resolved_image_id := _IMAGES [d_image_id ]) != d_image_id :
193+ dockerRequirement ["dockerImage_id" ] = resolved_image_id
194+ return True
195+
196+ docker_req = copy .deepcopy (dockerRequirement ) # thread safety
183197 if "CWL_SINGULARITY_CACHE" in os .environ :
184198 cache_folder = os .environ ["CWL_SINGULARITY_CACHE" ]
185199 elif is_version_2_6 () and "SINGULARITY_PULLFOLDER" in os .environ :
186200 cache_folder = os .environ ["SINGULARITY_PULLFOLDER" ]
187201
188- if "dockerFile" in dockerRequirement :
202+ if "dockerFile" in docker_req :
189203 if cache_folder is None : # if environment variables were not set
190204 cache_folder = create_tmp_dir (tmp_outdir_prefix )
191205
192206 absolute_path = os .path .abspath (cache_folder )
193- if "dockerImageId" in dockerRequirement :
194- image_name = dockerRequirement ["dockerImageId" ]
207+ if "dockerImageId" in docker_req :
208+ image_name = docker_req ["dockerImageId" ]
195209 image_path = os .path .join (absolute_path , image_name )
196210 if os .path .exists (image_path ):
197211 found = True
@@ -204,19 +218,19 @@ def get_image(
204218 # FATAL: Unable to create build: 'noexec' mount option set on
205219 # /tmp, temporary root filesystem won't be usable at this location
206220 with open (dockerfile_path , "w" ) as dfile :
207- dfile .write (dockerRequirement ["dockerFile" ])
221+ dfile .write (docker_req ["dockerFile" ])
208222
209223 singularityfile = SingularityWriter (DockerParser (dockerfile_path ).parse ()).convert ()
210224 with open (singularityfile_path , "w" ) as file :
211225 file .write (singularityfile )
212226
213227 os .environ ["APPTAINER_TMPDIR" ] = absolute_path
214228 singularity_options = ["--fakeroot" ] if not shutil .which ("proot" ) else []
215- if "dockerImageId" in dockerRequirement :
229+ if "dockerImageId" in docker_req :
216230 Client .build (
217231 recipe = singularityfile_path ,
218232 build_folder = absolute_path ,
219- image = dockerRequirement ["dockerImageId" ],
233+ image = docker_req ["dockerImageId" ],
220234 sudo = False ,
221235 options = singularity_options ,
222236 )
@@ -228,25 +242,25 @@ def get_image(
228242 options = singularity_options ,
229243 )
230244 found = True
231- 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" ])
245+ elif "dockerImageId" not in docker_req and "dockerPull" in docker_req :
246+ match = re .search (pattern = r"([a-z]*://)" , string = docker_req ["dockerPull" ])
247+ img_name = _normalize_image_id (docker_req ["dockerPull" ])
234248 candidates .append (img_name )
235249 if is_version_3_or_newer ():
236- sif_name = _normalize_sif_id (dockerRequirement ["dockerPull" ])
250+ sif_name = _normalize_sif_id (docker_req ["dockerPull" ])
237251 candidates .append (sif_name )
238- dockerRequirement ["dockerImageId" ] = sif_name
252+ docker_req ["dockerImageId" ] = sif_name
239253 else :
240- dockerRequirement ["dockerImageId" ] = img_name
254+ docker_req ["dockerImageId" ] = img_name
241255 if not match :
242- dockerRequirement ["dockerPull" ] = "docker://" + dockerRequirement ["dockerPull" ]
243- elif "dockerImageId" in dockerRequirement :
244- if os .path .isfile (dockerRequirement ["dockerImageId" ]):
256+ docker_req ["dockerPull" ] = "docker://" + docker_req ["dockerPull" ]
257+ elif "dockerImageId" in docker_req :
258+ if os .path .isfile (docker_req ["dockerImageId" ]):
245259 found = True
246- candidates .append (dockerRequirement ["dockerImageId" ])
247- candidates .append (_normalize_image_id (dockerRequirement ["dockerImageId" ]))
260+ candidates .append (docker_req ["dockerImageId" ])
261+ candidates .append (_normalize_image_id (docker_req ["dockerImageId" ]))
248262 if is_version_3_or_newer ():
249- candidates .append (_normalize_sif_id (dockerRequirement ["dockerImageId" ]))
263+ candidates .append (_normalize_sif_id (docker_req ["dockerImageId" ]))
250264
251265 targets = [os .getcwd ()]
252266 if "CWL_SINGULARITY_CACHE" in os .environ :
@@ -263,11 +277,11 @@ def get_image(
263277 "Using local copy of Singularity image found in %s" ,
264278 dirpath ,
265279 )
266- dockerRequirement ["dockerImageId" ] = path
280+ docker_req ["dockerImageId" ] = path
267281 found = True
268282 if (force_pull or not found ) and pull_image :
269283 cmd : list [str ] = []
270- if "dockerPull" in dockerRequirement :
284+ if "dockerPull" in docker_req :
271285 if cache_folder :
272286 env = os .environ .copy ()
273287 if is_version_2_6 ():
@@ -277,23 +291,23 @@ def get_image(
277291 "pull" ,
278292 "--force" ,
279293 "--name" ,
280- dockerRequirement ["dockerImageId" ],
281- str (dockerRequirement ["dockerPull" ]),
294+ docker_req ["dockerImageId" ],
295+ str (docker_req ["dockerPull" ]),
282296 ]
283297 else :
284298 cmd = [
285299 "singularity" ,
286300 "pull" ,
287301 "--force" ,
288302 "--name" ,
289- "{}/{}" .format (cache_folder , dockerRequirement ["dockerImageId" ]),
290- str (dockerRequirement ["dockerPull" ]),
303+ "{}/{}" .format (cache_folder , docker_req ["dockerImageId" ]),
304+ str (docker_req ["dockerPull" ]),
291305 ]
292306
293307 _logger .info (str (cmd ))
294308 check_call (cmd , env = env , stdout = sys .stderr ) # nosec
295- dockerRequirement ["dockerImageId" ] = "{}/{}" .format (
296- cache_folder , dockerRequirement ["dockerImageId" ]
309+ docker_req ["dockerImageId" ] = "{}/{}" .format (
310+ cache_folder , docker_req ["dockerImageId" ]
297311 )
298312 found = True
299313 else :
@@ -302,44 +316,47 @@ def get_image(
302316 "pull" ,
303317 "--force" ,
304318 "--name" ,
305- str (dockerRequirement ["dockerImageId" ]),
306- str (dockerRequirement ["dockerPull" ]),
319+ str (docker_req ["dockerImageId" ]),
320+ str (docker_req ["dockerPull" ]),
307321 ]
308322 _logger .info (str (cmd ))
309323 check_call (cmd , stdout = sys .stderr ) # nosec
310324 found = True
311325
312- elif "dockerLoad" in dockerRequirement :
326+ elif "dockerLoad" in docker_req :
313327 if is_version_3_1_or_newer ():
314- if "dockerImageId" in dockerRequirement :
315- name = "{}.sif" .format (dockerRequirement ["dockerImageId" ])
328+ if "dockerImageId" in docker_req :
329+ name = "{}.sif" .format (docker_req ["dockerImageId" ])
316330 else :
317- name = "{}.sif" .format (dockerRequirement ["dockerLoad" ])
331+ name = "{}.sif" .format (docker_req ["dockerLoad" ])
318332 cmd = [
319333 "singularity" ,
320334 "build" ,
321335 name ,
322- "docker-archive://{}" .format (dockerRequirement ["dockerLoad" ]),
336+ "docker-archive://{}" .format (docker_req ["dockerLoad" ]),
323337 ]
324338 _logger .info (str (cmd ))
325339 check_call (cmd , stdout = sys .stderr ) # nosec
326340 found = True
327- dockerRequirement ["dockerImageId" ] = name
341+ docker_req ["dockerImageId" ] = name
328342 else :
329- raise SourceLine (
330- dockerRequirement , "dockerLoad" , WorkflowException , debug
331- ).makeError (
343+ raise SourceLine (docker_req , "dockerLoad" , WorkflowException , debug ).makeError (
332344 "dockerLoad is not currently supported when using the "
333345 "Singularity runtime (version less than 3.1) for Docker containers."
334346 )
335- elif "dockerImport" in dockerRequirement :
336- raise SourceLine (
337- dockerRequirement , "dockerImport" , WorkflowException , debug
338- ).makeError (
347+ elif "dockerImport" in docker_req :
348+ raise SourceLine (docker_req , "dockerImport" , WorkflowException , debug ).makeError (
339349 "dockerImport is not currently supported when using the "
340350 "Singularity runtime for Docker containers."
341351 )
342-
352+ if found :
353+ with _IMAGES_LOCK :
354+ if "dockerImageId" in dockerRequirement :
355+ _IMAGES [dockerRequirement ["dockerImageId" ]] = docker_req ["dockerImageId" ]
356+ dockerRequirement .clear ()
357+ dockerRequirement |= docker_req
358+ if "dockerImageId" in docker_req :
359+ _IMAGES [docker_req ["dockerImageId" ]] = docker_req ["dockerImageId" ]
343360 return found
344361
345362 def get_from_requirements (
0 commit comments