Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/galaxy/tool_util/deps/container_resolvers/mulled.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """This module describes the :class:`MulledContainerResolver` ContainerResolver plugin.""" | |
2 | |
3 import logging | |
4 import os | |
5 import subprocess | |
6 from typing import NamedTuple, Optional | |
7 | |
8 from galaxy.util import ( | |
9 safe_makedirs, | |
10 string_as_bool, | |
11 unicodify, | |
12 which, | |
13 ) | |
14 from galaxy.util.commands import shell | |
15 from ..container_classes import CONTAINER_CLASSES | |
16 from ..container_resolvers import ( | |
17 ContainerResolver, | |
18 ) | |
19 from ..docker_util import build_docker_images_command | |
20 from ..mulled.mulled_build import ( | |
21 DEFAULT_CHANNELS, | |
22 ensure_installed, | |
23 InvolucroContext, | |
24 mull_targets, | |
25 ) | |
26 from ..mulled.mulled_build_tool import requirements_to_mulled_targets | |
27 from ..mulled.util import ( | |
28 mulled_tags_for, | |
29 split_tag, | |
30 v1_image_name, | |
31 v2_image_name, | |
32 version_sorted, | |
33 ) | |
34 from ..requirements import ( | |
35 ContainerDescription, | |
36 DEFAULT_CONTAINER_SHELL, | |
37 ) | |
38 | |
39 log = logging.getLogger(__name__) | |
40 | |
41 | |
42 class CachedMulledImageSingleTarget(NamedTuple): | |
43 package_name: str | |
44 version: str | |
45 build: str | |
46 image_identifier: str | |
47 | |
48 multi_target: bool = False | |
49 | |
50 | |
51 class CachedV1MulledImageMultiTarget(NamedTuple): | |
52 hash: str | |
53 build: str | |
54 image_identifier: str | |
55 | |
56 multi_target: str = "v1" | |
57 | |
58 | |
59 class CachedV2MulledImageMultiTarget(NamedTuple): | |
60 image_name: str | |
61 version_hash: str | |
62 build: str | |
63 image_identifier: str | |
64 | |
65 multi_target: str = "v2" | |
66 | |
67 @property | |
68 def package_hash(target): | |
69 # Make this work for Singularity file name or fully qualified Docker repository | |
70 # image names. | |
71 image_name = target.image_name | |
72 if "/" not in image_name: | |
73 return image_name | |
74 else: | |
75 return image_name.rsplit("/")[-1] | |
76 | |
77 | |
78 def list_docker_cached_mulled_images(namespace=None, hash_func="v2", resolution_cache=None): | |
79 cache_key = "galaxy.tool_util.deps.container_resolvers.mulled:cached_images" | |
80 if resolution_cache is not None and cache_key in resolution_cache: | |
81 images_and_versions = resolution_cache.get(cache_key) | |
82 else: | |
83 command = build_docker_images_command(truncate=True, sudo=False, to_str=False) | |
84 try: | |
85 images_and_versions = unicodify(subprocess.check_output(command)).strip().splitlines() | |
86 except subprocess.CalledProcessError: | |
87 log.info("Call to `docker images` failed, configured container resolution may be broken") | |
88 return [] | |
89 images_and_versions = [":".join(l.split()[0:2]) for l in images_and_versions[1:]] | |
90 if resolution_cache is not None: | |
91 resolution_cache[cache_key] = images_and_versions | |
92 | |
93 def output_line_to_image(line): | |
94 image = identifier_to_cached_target(line, hash_func, namespace=namespace) | |
95 return image | |
96 | |
97 name_filter = get_filter(namespace) | |
98 sorted_images = version_sorted([_ for _ in filter(name_filter, images_and_versions)]) | |
99 raw_images = (output_line_to_image(_) for _ in sorted_images) | |
100 return [i for i in raw_images if i is not None] | |
101 | |
102 | |
103 def identifier_to_cached_target(identifier, hash_func, namespace=None): | |
104 if ":" in identifier: | |
105 image_name, version = identifier.rsplit(":", 1) | |
106 else: | |
107 image_name = identifier | |
108 version = None | |
109 | |
110 if not version or version == "latest": | |
111 version = None | |
112 | |
113 image = None | |
114 prefix = "" | |
115 if namespace is not None: | |
116 prefix = "quay.io/%s/" % namespace | |
117 if image_name.startswith(prefix + "mulled-v1-"): | |
118 if hash_func == "v2": | |
119 return None | |
120 | |
121 hash = image_name | |
122 build = None | |
123 if version and version.isdigit(): | |
124 build = version | |
125 image = CachedV1MulledImageMultiTarget(hash, build, identifier) | |
126 elif image_name.startswith(prefix + "mulled-v2-"): | |
127 if hash_func == "v1": | |
128 return None | |
129 | |
130 version_hash = None | |
131 build = None | |
132 | |
133 if version and "-" in version: | |
134 version_hash, build = version.rsplit("-", 1) | |
135 elif version.isdigit(): | |
136 version_hash, build = None, version | |
137 elif version: | |
138 log.debug("Unparsable mulled image tag encountered [%s]" % version) | |
139 | |
140 image = CachedV2MulledImageMultiTarget(image_name, version_hash, build, identifier) | |
141 else: | |
142 build = None | |
143 if version and "--" in version: | |
144 version, build = split_tag(version) | |
145 if prefix and image_name.startswith(prefix): | |
146 image_name = image_name[len(prefix):] | |
147 image = CachedMulledImageSingleTarget(image_name, version, build, identifier) | |
148 return image | |
149 | |
150 | |
151 def list_cached_mulled_images_from_path(directory, hash_func="v2"): | |
152 contents = os.listdir(directory) | |
153 sorted_images = version_sorted(contents) | |
154 raw_images = map(lambda name: identifier_to_cached_target(name, hash_func), sorted_images) | |
155 return [i for i in raw_images if i is not None] | |
156 | |
157 | |
158 def get_filter(namespace): | |
159 prefix = "quay.io/" if namespace is None else "quay.io/%s" % namespace | |
160 return lambda name: name.startswith(prefix) and name.count("/") == 2 | |
161 | |
162 | |
163 def find_best_matching_cached_image(targets, cached_images, hash_func): | |
164 if len(targets) == 0: | |
165 return None | |
166 | |
167 image = None | |
168 if len(targets) == 1: | |
169 target = targets[0] | |
170 for cached_image in cached_images: | |
171 if cached_image.multi_target: | |
172 continue | |
173 if not cached_image.package_name == target.package_name: | |
174 continue | |
175 if not target.version or target.version == cached_image.version: | |
176 image = cached_image | |
177 break | |
178 elif hash_func == "v2": | |
179 name = v2_image_name(targets) | |
180 if ":" in name: | |
181 package_hash, version_hash = name.split(":", 2) | |
182 else: | |
183 package_hash, version_hash = name, None | |
184 | |
185 for cached_image in cached_images: | |
186 if cached_image.multi_target != "v2": | |
187 continue | |
188 | |
189 if version_hash is None: | |
190 # Just match on package hash... | |
191 if package_hash == cached_image.package_hash: | |
192 image = cached_image | |
193 break | |
194 else: | |
195 # Match on package and version hash... | |
196 if package_hash == cached_image.package_hash and version_hash == cached_image.version_hash: | |
197 image = cached_image | |
198 break | |
199 | |
200 elif hash_func == "v1": | |
201 name = v1_image_name(targets) | |
202 for cached_image in cached_images: | |
203 if cached_image.multi_target != "v1": | |
204 continue | |
205 | |
206 if name == cached_image.hash: | |
207 image = cached_image | |
208 break | |
209 return image | |
210 | |
211 | |
212 def docker_cached_container_description(targets, namespace, hash_func="v2", shell=DEFAULT_CONTAINER_SHELL, resolution_cache=None): | |
213 if len(targets) == 0: | |
214 return None | |
215 | |
216 cached_images = list_docker_cached_mulled_images(namespace, hash_func=hash_func, resolution_cache=resolution_cache) | |
217 image = find_best_matching_cached_image(targets, cached_images, hash_func) | |
218 | |
219 container = None | |
220 if image: | |
221 container = ContainerDescription( | |
222 image.image_identifier, | |
223 type="docker", | |
224 shell=shell, | |
225 ) | |
226 | |
227 return container | |
228 | |
229 | |
230 def singularity_cached_container_description(targets, cache_directory, hash_func="v2", shell=DEFAULT_CONTAINER_SHELL): | |
231 if len(targets) == 0: | |
232 return None | |
233 | |
234 if not os.path.exists(cache_directory): | |
235 return None | |
236 | |
237 cached_images = list_cached_mulled_images_from_path(cache_directory, hash_func=hash_func) | |
238 image = find_best_matching_cached_image(targets, cached_images, hash_func) | |
239 | |
240 container = None | |
241 if image: | |
242 container = ContainerDescription( | |
243 os.path.abspath(os.path.join(cache_directory, image.image_identifier)), | |
244 type="singularity", | |
245 shell=shell, | |
246 ) | |
247 | |
248 return container | |
249 | |
250 | |
251 def targets_to_mulled_name(targets, hash_func, namespace, resolution_cache=None, session=None): | |
252 unresolved_cache_key = "galaxy.tool_util.deps.container_resolvers.mulled:unresolved" | |
253 if resolution_cache is not None: | |
254 if unresolved_cache_key not in resolution_cache: | |
255 resolution_cache[unresolved_cache_key] = set() | |
256 unresolved_cache = resolution_cache.get(unresolved_cache_key) | |
257 else: | |
258 unresolved_cache = set() | |
259 | |
260 mulled_resolution_cache = None | |
261 if resolution_cache and hasattr(resolution_cache, 'mulled_resolution_cache'): | |
262 mulled_resolution_cache = resolution_cache.mulled_resolution_cache | |
263 | |
264 name = None | |
265 | |
266 def cached_name(cache_key): | |
267 if mulled_resolution_cache: | |
268 if cache_key in mulled_resolution_cache: | |
269 return resolution_cache.get(cache_key) | |
270 return None | |
271 | |
272 if len(targets) == 1: | |
273 target = targets[0] | |
274 target_version = target.version | |
275 cache_key = f"ns[{namespace}]__single__{target.package_name}__@__{target_version}" | |
276 if cache_key in unresolved_cache: | |
277 return None | |
278 name = cached_name(cache_key) | |
279 if name: | |
280 return name | |
281 | |
282 tags = mulled_tags_for(namespace, target.package_name, resolution_cache=resolution_cache, session=session) | |
283 | |
284 if tags: | |
285 for tag in tags: | |
286 if '--' in tag: | |
287 version, _ = split_tag(tag) | |
288 else: | |
289 version = tag | |
290 if target_version and version == target_version: | |
291 name = f"{target.package_name}:{tag}" | |
292 break | |
293 | |
294 else: | |
295 def first_tag_if_available(image_name): | |
296 if ":" in image_name: | |
297 repo_name, tag_prefix = image_name.split(":", 2) | |
298 else: | |
299 repo_name = image_name | |
300 tag_prefix = None | |
301 tags = mulled_tags_for(namespace, repo_name, tag_prefix=tag_prefix, resolution_cache=resolution_cache, session=session) | |
302 return tags[0] if tags else None | |
303 | |
304 if hash_func == "v2": | |
305 base_image_name = v2_image_name(targets) | |
306 elif hash_func == "v1": | |
307 base_image_name = v1_image_name(targets) | |
308 else: | |
309 raise Exception("Unimplemented mulled hash_func [%s]" % hash_func) | |
310 | |
311 cache_key = f"ns[{namespace}]__{hash_func}__{base_image_name}" | |
312 if cache_key in unresolved_cache: | |
313 return None | |
314 name = cached_name(cache_key) | |
315 if name: | |
316 return name | |
317 | |
318 tag = first_tag_if_available(base_image_name) | |
319 if tag: | |
320 if ":" in base_image_name: | |
321 assert hash_func != "v1" | |
322 # base_image_name of form <package_hash>:<version_hash>, expand tag | |
323 # to include build number in tag. | |
324 name = "{}:{}".format(base_image_name.split(":")[0], tag) | |
325 else: | |
326 # base_image_name of form <package_hash>, simply add build number | |
327 # as tag to fully qualify image. | |
328 name = f"{base_image_name}:{tag}" | |
329 | |
330 if name and mulled_resolution_cache: | |
331 mulled_resolution_cache.put(cache_key, name) | |
332 | |
333 if name is None: | |
334 unresolved_cache.add(name) | |
335 | |
336 return name | |
337 | |
338 | |
339 class CliContainerResolver(ContainerResolver): | |
340 | |
341 container_type = 'docker' | |
342 cli = 'docker' | |
343 | |
344 def __init__(self, *args, **kwargs): | |
345 self._cli_available = bool(which(self.cli)) | |
346 super().__init__(*args, **kwargs) | |
347 | |
348 @property | |
349 def cli_available(self): | |
350 return self._cli_available | |
351 | |
352 @cli_available.setter | |
353 def cli_available(self, value): | |
354 if not value: | |
355 log.info('{} CLI not available, cannot list or pull images in Galaxy process. Does not impact kubernetes.'.format(self.cli)) | |
356 self._cli_available = value | |
357 | |
358 | |
359 class SingularityCliContainerResolver(CliContainerResolver): | |
360 | |
361 container_type = 'singularity' | |
362 cli = 'singularity' | |
363 | |
364 def __init__(self, *args, **kwargs): | |
365 super().__init__(*args, **kwargs) | |
366 self.cache_directory = kwargs.get("cache_directory", os.path.join(kwargs['app_info'].container_image_cache_path, "singularity", "mulled")) | |
367 safe_makedirs(self.cache_directory) | |
368 | |
369 | |
370 class CachedMulledDockerContainerResolver(CliContainerResolver): | |
371 | |
372 resolver_type = "cached_mulled" | |
373 shell = '/bin/bash' | |
374 | |
375 def __init__(self, app_info=None, namespace="biocontainers", hash_func="v2", **kwds): | |
376 super().__init__(app_info=app_info, **kwds) | |
377 self.namespace = namespace | |
378 self.hash_func = hash_func | |
379 | |
380 def resolve(self, enabled_container_types, tool_info, **kwds): | |
381 if not self.cli_available or tool_info.requires_galaxy_python_environment or self.container_type not in enabled_container_types: | |
382 return None | |
383 | |
384 targets = mulled_targets(tool_info) | |
385 resolution_cache = kwds.get("resolution_cache") | |
386 return docker_cached_container_description(targets, self.namespace, hash_func=self.hash_func, shell=self.shell, resolution_cache=resolution_cache) | |
387 | |
388 def __str__(self): | |
389 return "CachedMulledDockerContainerResolver[namespace=%s]" % self.namespace | |
390 | |
391 | |
392 class CachedMulledSingularityContainerResolver(SingularityCliContainerResolver): | |
393 | |
394 resolver_type = "cached_mulled_singularity" | |
395 shell = '/bin/bash' | |
396 | |
397 def __init__(self, app_info=None, hash_func="v2", **kwds): | |
398 super().__init__(app_info=app_info, **kwds) | |
399 self.hash_func = hash_func | |
400 | |
401 def resolve(self, enabled_container_types, tool_info, **kwds): | |
402 if tool_info.requires_galaxy_python_environment or self.container_type not in enabled_container_types: | |
403 return None | |
404 | |
405 targets = mulled_targets(tool_info) | |
406 return singularity_cached_container_description(targets, self.cache_directory, hash_func=self.hash_func, shell=self.shell) | |
407 | |
408 def __str__(self): | |
409 return "CachedMulledSingularityContainerResolver[cache_directory=%s]" % self.cache_directory | |
410 | |
411 | |
412 class MulledDockerContainerResolver(CliContainerResolver): | |
413 """Look for mulled images matching tool dependencies.""" | |
414 | |
415 resolver_type = "mulled" | |
416 shell = '/bin/bash' | |
417 protocol: Optional[str] = None | |
418 | |
419 def __init__(self, app_info=None, namespace="biocontainers", hash_func="v2", auto_install=True, **kwds): | |
420 super().__init__(app_info=app_info, **kwds) | |
421 self.namespace = namespace | |
422 self.hash_func = hash_func | |
423 self.auto_install = string_as_bool(auto_install) | |
424 | |
425 def cached_container_description(self, targets, namespace, hash_func, resolution_cache): | |
426 try: | |
427 return docker_cached_container_description(targets, namespace, hash_func, resolution_cache) | |
428 except subprocess.CalledProcessError: | |
429 # We should only get here if a docker binary is available, but command quits with a non-zero exit code, | |
430 # e.g if the docker daemon is not available | |
431 log.exception('An error occured while listing cached docker image. Docker daemon may need to be restarted.') | |
432 return None | |
433 | |
434 def pull(self, container): | |
435 if self.cli_available: | |
436 command = container.build_pull_command() | |
437 shell(command) | |
438 | |
439 @property | |
440 def can_list_containers(self): | |
441 return self.cli_available | |
442 | |
443 def resolve(self, enabled_container_types, tool_info, install=False, session=None, **kwds): | |
444 resolution_cache = kwds.get("resolution_cache") | |
445 if tool_info.requires_galaxy_python_environment or self.container_type not in enabled_container_types: | |
446 return None | |
447 | |
448 targets = mulled_targets(tool_info) | |
449 if len(targets) == 0: | |
450 return None | |
451 | |
452 name = targets_to_mulled_name(targets=targets, hash_func=self.hash_func, namespace=self.namespace, resolution_cache=resolution_cache, session=session) | |
453 if name: | |
454 container_id = f"quay.io/{self.namespace}/{name}" | |
455 if self.protocol: | |
456 container_id = f"{self.protocol}{container_id}" | |
457 container_description = ContainerDescription( | |
458 container_id, | |
459 type=self.container_type, | |
460 shell=self.shell, | |
461 ) | |
462 if self.can_list_containers: | |
463 if install and not self.cached_container_description( | |
464 targets, | |
465 namespace=self.namespace, | |
466 hash_func=self.hash_func, | |
467 resolution_cache=resolution_cache, | |
468 ): | |
469 destination_info = {} | |
470 destination_for_container_type = kwds.get('destination_for_container_type') | |
471 if destination_for_container_type: | |
472 destination_info = destination_for_container_type(self.container_type) | |
473 container = CONTAINER_CLASSES[self.container_type](container_description.identifier, | |
474 self.app_info, | |
475 tool_info, | |
476 destination_info, | |
477 {}, | |
478 container_description) | |
479 self.pull(container) | |
480 if not self.auto_install: | |
481 container_description = self.cached_container_description( | |
482 targets, | |
483 namespace=self.namespace, | |
484 hash_func=self.hash_func, | |
485 resolution_cache=resolution_cache, | |
486 ) or container_description | |
487 return container_description | |
488 | |
489 def __str__(self): | |
490 return "MulledDockerContainerResolver[namespace=%s]" % self.namespace | |
491 | |
492 | |
493 class MulledSingularityContainerResolver(SingularityCliContainerResolver, MulledDockerContainerResolver): | |
494 | |
495 resolver_type = "mulled_singularity" | |
496 protocol = 'docker://' | |
497 | |
498 def __init__(self, app_info=None, namespace="biocontainers", hash_func="v2", auto_install=True, **kwds): | |
499 super().__init__(app_info=app_info, **kwds) | |
500 self.namespace = namespace | |
501 self.hash_func = hash_func | |
502 self.auto_install = string_as_bool(auto_install) | |
503 | |
504 def cached_container_description(self, targets, namespace, hash_func, resolution_cache): | |
505 return singularity_cached_container_description(targets, | |
506 cache_directory=self.cache_directory, | |
507 hash_func=hash_func) | |
508 | |
509 @property | |
510 def can_list_containers(self): | |
511 # Only needs access to path, doesn't require CLI | |
512 return True | |
513 | |
514 def pull(self, container): | |
515 if self.cli_available: | |
516 cmds = container.build_mulled_singularity_pull_command(cache_directory=self.cache_directory, namespace=self.namespace) | |
517 shell(cmds=cmds) | |
518 | |
519 def __str__(self): | |
520 return "MulledSingularityContainerResolver[namespace=%s]" % self.namespace | |
521 | |
522 | |
523 class BuildMulledDockerContainerResolver(CliContainerResolver): | |
524 """Build for Docker mulled images matching tool dependencies.""" | |
525 | |
526 resolver_type = "build_mulled" | |
527 shell = '/bin/bash' | |
528 builds_on_resolution = True | |
529 | |
530 def __init__(self, app_info=None, namespace="local", hash_func="v2", auto_install=True, **kwds): | |
531 super().__init__(app_info=app_info, **kwds) | |
532 self._involucro_context_kwds = { | |
533 'involucro_bin': self._get_config_option("involucro_path", None) | |
534 } | |
535 self.namespace = namespace | |
536 self.hash_func = hash_func | |
537 self.auto_install = string_as_bool(auto_install) | |
538 self._mulled_kwds = { | |
539 'namespace': namespace, | |
540 'channels': self._get_config_option("mulled_channels", DEFAULT_CHANNELS), | |
541 'hash_func': self.hash_func, | |
542 'command': 'build-and-test', | |
543 } | |
544 self.auto_init = self._get_config_option("involucro_auto_init", True) | |
545 | |
546 def resolve(self, enabled_container_types, tool_info, install=False, **kwds): | |
547 if tool_info.requires_galaxy_python_environment or self.container_type not in enabled_container_types: | |
548 return None | |
549 | |
550 targets = mulled_targets(tool_info) | |
551 if len(targets) == 0: | |
552 return None | |
553 if self.auto_install or install: | |
554 mull_targets( | |
555 targets, | |
556 involucro_context=self._get_involucro_context(), | |
557 **self._mulled_kwds | |
558 ) | |
559 return docker_cached_container_description(targets, self.namespace, hash_func=self.hash_func, shell=self.shell) | |
560 | |
561 def _get_involucro_context(self): | |
562 involucro_context = InvolucroContext(**self._involucro_context_kwds) | |
563 self.enabled = ensure_installed(involucro_context, self.auto_init) | |
564 return involucro_context | |
565 | |
566 def __str__(self): | |
567 return "BuildDockerContainerResolver[namespace=%s]" % self.namespace | |
568 | |
569 | |
570 class BuildMulledSingularityContainerResolver(SingularityCliContainerResolver): | |
571 """Build for Singularity mulled images matching tool dependencies.""" | |
572 | |
573 resolver_type = "build_mulled_singularity" | |
574 shell = '/bin/bash' | |
575 builds_on_resolution = True | |
576 | |
577 def __init__(self, app_info=None, hash_func="v2", auto_install=True, **kwds): | |
578 super().__init__(app_info=app_info, **kwds) | |
579 self._involucro_context_kwds = { | |
580 'involucro_bin': self._get_config_option("involucro_path", None) | |
581 } | |
582 self.hash_func = hash_func | |
583 self.auto_install = string_as_bool(auto_install) | |
584 self._mulled_kwds = { | |
585 'channels': self._get_config_option("mulled_channels", DEFAULT_CHANNELS), | |
586 'hash_func': self.hash_func, | |
587 'command': 'build-and-test', | |
588 'singularity': True, | |
589 'singularity_image_dir': self.cache_directory, | |
590 } | |
591 self.auto_init = self._get_config_option("involucro_auto_init", True) | |
592 | |
593 def resolve(self, enabled_container_types, tool_info, install=False, **kwds): | |
594 if tool_info.requires_galaxy_python_environment or self.container_type not in enabled_container_types: | |
595 return None | |
596 | |
597 targets = mulled_targets(tool_info) | |
598 if len(targets) == 0: | |
599 return None | |
600 | |
601 if self.auto_install or install: | |
602 mull_targets( | |
603 targets, | |
604 involucro_context=self._get_involucro_context(), | |
605 **self._mulled_kwds | |
606 ) | |
607 return singularity_cached_container_description(targets, self.cache_directory, hash_func=self.hash_func, shell=self.shell) | |
608 | |
609 def _get_involucro_context(self): | |
610 involucro_context = InvolucroContext(**self._involucro_context_kwds) | |
611 self.enabled = ensure_installed(involucro_context, self.auto_init) | |
612 return involucro_context | |
613 | |
614 def __str__(self): | |
615 return "BuildSingularityContainerResolver[cache_directory=%s]" % self.cache_directory | |
616 | |
617 | |
618 def mulled_targets(tool_info): | |
619 return requirements_to_mulled_targets(tool_info.requirements) | |
620 | |
621 | |
622 __all__ = ( | |
623 "CachedMulledDockerContainerResolver", | |
624 "CachedMulledSingularityContainerResolver", | |
625 "MulledDockerContainerResolver", | |
626 "MulledSingularityContainerResolver", | |
627 "BuildMulledDockerContainerResolver", | |
628 "BuildMulledSingularityContainerResolver", | |
629 ) |