Coverage for cli/_container_runtime.py: 98.18%
39 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +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# Generated at (UTC): 2026-07-18T01:03:40Z
29# Flowchart(s) generated from this file:
30# * ``detect_container_runtime`` -> ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.html``
31# (PNG: ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.png``)
32# Regenerate with ``python diagrams/code_diagrams/generate.py``.
33# <pyflowchart-code-diagram> END
36logger = logging.getLogger(__name__)
38# Cached result for container runtime detection.
39#
40# Sentinel pattern: ``_UNCHECKED`` means the probe has not run yet;
41# any other value (including ``None``, which means "no runtime found")
42# is the cached result of the last probe. Using a single sentinel
43# instead of two separate ``_cache`` / ``_checked`` globals keeps the
44# cache state idempotent under concurrent first-callers and avoids
45# the static-analysis false positive on a stand-alone bool flag.
46_UNCHECKED: object = object()
47_container_runtime_cache: str | None | object = _UNCHECKED
50def detect_container_runtime() -> str | None:
51 """
52 Detect available container runtime (cached).
54 Returns:
55 Runtime name (``"docker"``, ``"finch"``, or ``"podman"``) if a
56 runtime is found and running, ``None`` if nothing is available.
58 Note:
59 If the ``CDK_DOCKER`` environment variable is set, that value
60 is returned without checking if the runtime is available.
61 """
62 global _container_runtime_cache
63 if _container_runtime_cache is not _UNCHECKED:
64 # ``_container_runtime_cache`` is narrowed to ``str | None`` once
65 # past the sentinel check, but mypy can't infer that across the
66 # ``object`` union. The runtime cast is explicit.
67 return _container_runtime_cache # type: ignore[return-value]
69 result = _detect_container_runtime_uncached()
70 _container_runtime_cache = result
71 return result
74def _detect_container_runtime_uncached() -> str | None:
75 """Uncached implementation of container runtime detection."""
76 # Check if CDK_DOCKER is already set
77 if os.environ.get("CDK_DOCKER"):
78 return os.environ["CDK_DOCKER"]
80 # Try docker first
81 if shutil.which("docker"):
82 # Verify docker is actually running
83 try:
84 result = subprocess.run(
85 ["docker", "info"],
86 capture_output=True,
87 timeout=5,
88 )
89 if result.returncode == 0:
90 return "docker"
91 except Exception as e:
92 logger.debug("docker info check failed: %s", e)
94 # Try finch as fallback
95 if shutil.which("finch"):
96 try:
97 result = subprocess.run(
98 ["finch", "info"],
99 capture_output=True,
100 timeout=5,
101 )
102 if result.returncode == 0:
103 return "finch"
104 except Exception as e:
105 logger.debug("finch info check failed: %s", e)
107 # Try podman as last resort
108 if shutil.which("podman"):
109 try:
110 result = subprocess.run(
111 ["podman", "info"],
112 capture_output=True,
113 timeout=5,
114 )
115 if result.returncode == 0: 115 ↛ 120line 115 didn't jump to line 120 because the condition on line 115 was always true
116 return "podman"
117 except Exception as e:
118 logger.debug("podman info check failed: %s", e)
120 return None