Coverage for cli/_container_runtime.py: 98%
39 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
1"""
2Container runtime detection (Docker, Finch, Podman) — shared helper.
4Originally part of cli/stacks.py for CDK asset bundling; extracted so
5the new cli/images.py ImageManager can reuse the cached detection
6without duplicating the probe logic.
8CDK requires a container runtime to build Lambda function assets, and
9the image registry uses the same runtime for ``docker build`` /
10``docker push`` calls. This module checks for available runtimes in
11order of preference and verifies they are actually running (not just
12installed).
14Priority order: docker > finch > podman.
16If the ``CDK_DOCKER`` environment variable is set, that value is
17returned without checking if the runtime is available.
18"""
20from __future__ import annotations
22import logging
23import os
24import shutil
25import subprocess
27# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit
28# Flowchart(s) generated from this file:
29# * ``detect_container_runtime`` -> ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.html``
30# (PNG: ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.png``)
31# Regenerate with ``python diagrams/code_diagrams/generate.py``.
32# <pyflowchart-code-diagram> END
35logger = logging.getLogger(__name__)
37# Cached result for container runtime detection.
38#
39# Sentinel pattern: ``_UNCHECKED`` means the probe has not run yet;
40# any other value (including ``None``, which means "no runtime found")
41# is the cached result of the last probe. Using a single sentinel
42# instead of two separate ``_cache`` / ``_checked`` globals keeps the
43# cache state idempotent under concurrent first-callers and avoids
44# the static-analysis false positive on a stand-alone bool flag.
45_UNCHECKED: object = object()
46_container_runtime_cache: str | None | object = _UNCHECKED
49def detect_container_runtime() -> str | None:
50 """
51 Detect available container runtime (cached).
53 Returns:
54 Runtime name (``"docker"``, ``"finch"``, or ``"podman"``) if a
55 runtime is found and running, ``None`` if nothing is available.
57 Note:
58 If the ``CDK_DOCKER`` environment variable is set, that value
59 is returned without checking if the runtime is available.
60 """
61 global _container_runtime_cache
62 if _container_runtime_cache is not _UNCHECKED:
63 # ``_container_runtime_cache`` is narrowed to ``str | None`` once
64 # past the sentinel check, but mypy can't infer that across the
65 # ``object`` union. The runtime cast is explicit.
66 return _container_runtime_cache # type: ignore[return-value]
68 result = _detect_container_runtime_uncached()
69 _container_runtime_cache = result
70 return result
73def _detect_container_runtime_uncached() -> str | None:
74 """Uncached implementation of container runtime detection."""
75 # Check if CDK_DOCKER is already set
76 if os.environ.get("CDK_DOCKER"):
77 return os.environ["CDK_DOCKER"]
79 # Try docker first
80 if shutil.which("docker"):
81 # Verify docker is actually running
82 try:
83 result = subprocess.run(
84 ["docker", "info"],
85 capture_output=True,
86 timeout=5,
87 )
88 if result.returncode == 0:
89 return "docker"
90 except Exception as e:
91 logger.debug("docker info check failed: %s", e)
93 # Try finch as fallback
94 if shutil.which("finch"):
95 try:
96 result = subprocess.run(
97 ["finch", "info"],
98 capture_output=True,
99 timeout=5,
100 )
101 if result.returncode == 0:
102 return "finch"
103 except Exception as e:
104 logger.debug("finch info check failed: %s", e)
106 # Try podman as last resort
107 if shutil.which("podman"):
108 try:
109 result = subprocess.run(
110 ["podman", "info"],
111 capture_output=True,
112 timeout=5,
113 )
114 if result.returncode == 0: 114 ↛ 119line 114 didn't jump to line 119 because the condition on line 114 was always true
115 return "podman"
116 except Exception as e:
117 logger.debug("podman info check failed: %s", e)
119 return None