Coverage for cli / _container_runtime.py: 100.00%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1""" 

2Container runtime detection (Docker, Finch, Podman) — shared helper. 

3 

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. 

7 

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). 

13 

14Priority order: docker > finch > podman. 

15 

16If the ``CDK_DOCKER`` environment variable is set, that value is 

17returned without checking if the runtime is available. 

18""" 

19 

20from __future__ import annotations 

21 

22import logging 

23import os 

24import shutil 

25import subprocess 

26 

27# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit 

28# Generated at (UTC): 2026-09-18T02:11:36Z 

29# Generated from Git commit: b8faa9689385cea16155a285a7f70cf6d488e512 

30# Flowchart(s) generated from this file: 

31# * ``detect_container_runtime`` -> ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.html`` 

32# (PNG: ``diagrams/code_diagrams/cli/_container_runtime.detect_container_runtime.png``) 

33# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/generate.py --code-only``. 

34# <pyflowchart-code-diagram> END 

35 

36 

37logger = logging.getLogger(__name__) 

38 

39# Cached result for container runtime detection. 

40# 

41# Sentinel pattern: ``_UNCHECKED`` means the probe has not run yet; 

42# any other value (including ``None``, which means "no runtime found") 

43# is the cached result of the last probe. Using a single sentinel 

44# instead of two separate ``_cache`` / ``_checked`` globals keeps the 

45# cache state idempotent under concurrent first-callers and avoids 

46# the static-analysis false positive on a stand-alone bool flag. 

47_UNCHECKED: object = object() 

48_container_runtime_cache: str | object | None = _UNCHECKED 

49 

50 

51def detect_container_runtime() -> str | None: 

52 """ 

53 Detect available container runtime (cached). 

54 

55 Returns: 

56 Runtime name (``"docker"``, ``"finch"``, or ``"podman"``) if a 

57 runtime is found and running, ``None`` if nothing is available. 

58 

59 Note: 

60 If the ``CDK_DOCKER`` environment variable is set, that value 

61 is returned without checking if the runtime is available. 

62 """ 

63 global _container_runtime_cache 

64 if _container_runtime_cache is not _UNCHECKED: 

65 # ``_container_runtime_cache`` is narrowed to ``str | None`` once 

66 # past the sentinel check, but mypy can't infer that across the 

67 # ``object`` union. The runtime cast is explicit. 

68 return _container_runtime_cache # type: ignore[return-value] 

69 

70 result = _detect_container_runtime_uncached() 

71 _container_runtime_cache = result 

72 return result 

73 

74 

75def _detect_container_runtime_uncached() -> str | None: 

76 """Uncached implementation of container runtime detection.""" 

77 # Check if CDK_DOCKER is already set 

78 if os.environ.get("CDK_DOCKER"): 

79 return os.environ["CDK_DOCKER"] 

80 

81 # Try docker first 

82 if shutil.which("docker"): 

83 # Verify docker is actually running 

84 try: 

85 result = subprocess.run( 

86 ["docker", "info"], 

87 capture_output=True, 

88 timeout=5, 

89 check=False, 

90 ) 

91 if result.returncode == 0: 

92 return "docker" 

93 except Exception as e: 

94 logger.debug("docker info check failed: %s", e) 

95 

96 # Try finch as fallback 

97 if shutil.which("finch"): 

98 try: 

99 result = subprocess.run( 

100 ["finch", "info"], 

101 capture_output=True, 

102 timeout=5, 

103 check=False, 

104 ) 

105 if result.returncode == 0: 

106 return "finch" 

107 except Exception as e: 

108 logger.debug("finch info check failed: %s", e) 

109 

110 # Try podman as last resort 

111 if shutil.which("podman"): 

112 try: 

113 result = subprocess.run( 

114 ["podman", "info"], 

115 capture_output=True, 

116 timeout=5, 

117 check=False, 

118 ) 

119 if result.returncode == 0: 

120 return "podman" 

121 except Exception as e: 

122 logger.debug("podman info check failed: %s", e) 

123 

124 return None 

125 

126 

127def container_runtime_error_message(*, allow_cdk_docker: bool = False) -> str: 

128 """Build an actionable "no runtime" message for the situation on this host. 

129 

130 Detection requires a runtime that is *running*, not merely installed, so the 

131 two failure modes need different advice and conflating them wastes real time: 

132 a stopped Finch VM was reported as "please install Finch" during a live 

133 release-validation run on 2026-08-26, on a machine that already had it. The 

134 install hint sent the reader to a page they had already followed. 

135 

136 So: if a runtime binary is on PATH but did not answer, say so and give the 

137 command that starts it. Only suggest installing when nothing is present. 

138 """ 

139 import shutil 

140 

141 installed = [name for name in ("docker", "finch", "podman") if shutil.which(name)] 

142 

143 if installed: 

144 start_hints = { 

145 "docker": "start Docker Desktop (or `open -a Docker` on macOS)", 

146 "finch": "finch vm start (first time: finch vm init)", 

147 "podman": "podman machine start (first time: podman machine init)", 

148 } 

149 lines = [ 

150 "No container runtime is running. These are installed but did not " 

151 f"respond: {', '.join(installed)}.", 

152 "", 

153 "Start one and retry:", 

154 ] 

155 lines += [f" - {name}: {start_hints[name]}" for name in installed] 

156 lines += [ 

157 "", 

158 "Detection runs `<runtime> info` with a 5s timeout, so a runtime whose " 

159 "VM is still booting also reports as unavailable — give it a moment " 

160 "and retry.", 

161 ] 

162 if allow_cdk_docker: 

163 lines.append("Alternatively set CDK_DOCKER=<path> to a runtime binary.") 

164 return "\n".join(lines) 

165 

166 lines = [ 

167 "No container runtime found. Install Docker, Finch, or Podman.", 

168 " - Docker: https://docs.docker.com/get-docker/", 

169 " - Finch: brew install finch && finch vm init && finch vm start", 

170 " - Podman: https://podman.io/getting-started/installation", 

171 ] 

172 if allow_cdk_docker: 

173 lines.append("Alternatively set CDK_DOCKER=<path> to a runtime binary.") 

174 return "\n".join(lines)