docker-stack 2.2.2__py3-none-any.whl → 2.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
docker_stack/cli.py CHANGED
@@ -266,15 +266,35 @@ def _format_names(names: object, *, no_trunc: bool) -> str:
266
266
  return ",".join(values)
267
267
 
268
268
 
269
+ DOCKER_HUB_NAME_PREFIXES = (
270
+ "docker.io/library/",
271
+ "index.docker.io/library/",
272
+ "docker.io/",
273
+ "index.docker.io/",
274
+ )
275
+
276
+
277
+ def _familiar_image_name(value: str) -> str:
278
+ for prefix in DOCKER_HUB_NAME_PREFIXES:
279
+ if value.startswith(prefix):
280
+ return value[len(prefix) :]
281
+ return value
282
+
283
+
269
284
  def _format_image(image: object, image_id: object, *, no_trunc: bool) -> str:
270
285
  value = str(image or "")
271
286
  if not value:
272
287
  return "<no image>"
273
288
  if no_trunc:
274
289
  return value
290
+ if value.startswith("sha256:"):
291
+ return _short_id(value)
275
292
  identifier = str(image_id or "")
276
293
  if identifier and _short_id(identifier) == _short_id(value):
277
294
  return _short_id(value)
295
+ if "@" in value:
296
+ # docker ps keeps `name[:tag]` and drops the pinned digest unless --no-trunc.
297
+ return _familiar_image_name(value.split("@", 1)[0])
278
298
  return value
279
299
 
280
300
 
@@ -330,6 +350,38 @@ def _print_cluster_containers(payload: Dict[str, object], *, no_trunc: bool = Fa
330
350
  return 1 if errors else 0
331
351
 
332
352
 
353
+ def _exec_process(command: List[str], env: Optional[Dict[str, str]] = None) -> int:
354
+ """Replace this process with ``command`` so it owns the terminal, its signals, and its exit code.
355
+
356
+ Proxying through ``subprocess.run`` keeps Python in the foreground process group: Ctrl+C is
357
+ delivered to the proxy as well as to the child, which surfaces as a ``KeyboardInterrupt``
358
+ traceback, and a signal-killed child reports a negative return code that ``sys.exit`` turns into
359
+ a nonsense status (``-2`` becomes ``254`` instead of ``130``). ``exec`` removes the middleman.
360
+ Only POSIX execs: on Windows ``exec`` detaches the child and hands control straight back, which
361
+ would break the caller's wait semantics.
362
+
363
+ Returns only on failure; the caller keeps its ``int`` contract for that case.
364
+ """
365
+ if os.name != "posix":
366
+ return subprocess.run(command, check=False, env=env).returncode
367
+ # exec discards Python's buffers, so anything already printed has to reach the fd first.
368
+ sys.stdout.flush()
369
+ sys.stderr.flush()
370
+ try:
371
+ if env is None:
372
+ os.execvp(command[0], command)
373
+ else:
374
+ os.execvpe(command[0], command, env)
375
+ except OSError as exc:
376
+ print(f"docker-stack: cannot execute {command[0]}: {exc}", file=sys.stderr)
377
+ return 127
378
+
379
+
380
+ def _exec_docker(values: List[str]) -> int:
381
+ """Hand the command to the real Docker CLI, replacing this process."""
382
+ return _exec_process(["docker", *values])
383
+
384
+
333
385
  def _managed_docker(arguments: List[str]) -> int:
334
386
  values = list(arguments)
335
387
  if values and values[0] == "--":
@@ -358,11 +410,11 @@ def _managed_docker(arguments: List[str]) -> int:
358
410
  value in {"-q", "--quiet", "--format"} or value.startswith("--format=")
359
411
  for value in command
360
412
  ):
361
- return subprocess.run(["docker", *values], check=False).returncode
413
+ return _exec_docker(values)
362
414
  try:
363
415
  client = discover_manager_client()
364
416
  if client is None or not client.supports(FEATURE_CLUSTER_CONTAINER_CLI):
365
- return subprocess.run(["docker", *values], check=False).returncode
417
+ return _exec_docker(values)
366
418
  filters = []
367
419
  all_containers = False
368
420
  latest = False
@@ -391,7 +443,7 @@ def _managed_docker(arguments: List[str]) -> int:
391
443
  elif value.startswith("--last="):
392
444
  limit = int(value.split("=", 1)[1])
393
445
  else:
394
- return subprocess.run(["docker", *values], check=False).returncode
446
+ return _exec_docker(values)
395
447
  index += 1
396
448
  payload = client.list_containers(
397
449
  all_containers=all_containers,
@@ -1754,7 +1806,7 @@ def open_context_shell(config_dir: Path, context_name: str) -> int:
1754
1806
  env["DOCKER_CONTEXT"] = context_name
1755
1807
  env["DOCKER_MANAGER_CONTEXT_NAME"] = context_name
1756
1808
  shell = env.get("SHELL", "").strip() or "/bin/bash"
1757
- return subprocess.run([shell, "-i"], check=False, env=env).returncode
1809
+ return _exec_process([shell, "-i"], env=env)
1758
1810
 
1759
1811
 
1760
1812
  def active_shell_config_dir(context_name: str) -> Optional[Path]:
@@ -1785,6 +1837,29 @@ def _error_output(exc: subprocess.CalledProcessError) -> str:
1785
1837
  return "\n".join(value for value in values if value)
1786
1838
 
1787
1839
 
1840
+ _MANAGER_VERSION_PROBE_PATTERN = re.compile(r"manager request failed \(\w+ /version\)", re.IGNORECASE)
1841
+
1842
+
1843
+ def _humanize_manager_error(message: str) -> str:
1844
+ """Render a manager JSON error body as prose instead of dumping the raw payload."""
1845
+ match = re.search(r"^(.*?: HTTP \d+): (\{.*\})$", message, re.DOTALL)
1846
+ if not match:
1847
+ return message
1848
+ try:
1849
+ body = json.loads(match.group(2))
1850
+ except json.JSONDecodeError:
1851
+ return message
1852
+ if not isinstance(body, dict):
1853
+ return message
1854
+ detail = str(body.get("message", "")).strip()
1855
+ if not detail:
1856
+ return message
1857
+ incident = str(body.get("incident_id") or body.get("trace_id") or "").strip()
1858
+ if incident:
1859
+ return f"{match.group(1)}: {detail} (incident {incident})"
1860
+ return f"{match.group(1)}: {detail}"
1861
+
1862
+
1788
1863
  def _manager_deploy_suggestion(message: str) -> Optional[str]:
1789
1864
  if GITHUB_WORKFLOW_RESTRICTED_MESSAGE in message:
1790
1865
  return (
@@ -1804,10 +1879,16 @@ def _manager_deploy_suggestion(message: str) -> Optional[str]:
1804
1879
  "by this stack. Either allow that external network in the Docker-Manager deployment rule, attach the service to "
1805
1880
  "a stack-owned network, or relabel/recreate the existing network with the expected stack ownership before deploying."
1806
1881
  )
1807
- if "manager request failed" in lowered and "/version" in lowered:
1882
+ if "stack not found" in lowered:
1883
+ return (
1884
+ "Suggestion: the manager was reached but has no stack by that name in that namespace. "
1885
+ "Run 'docker-stack ls' to list stacks in the current namespace, or 'docker-stack ls -A' to search every namespace."
1886
+ )
1887
+ if _MANAGER_VERSION_PROBE_PATTERN.search(message):
1808
1888
  return (
1809
- "Suggestion: check that the manager URL is reachable from the runner and that the setup action exported "
1810
- "DOCKER_MANAGER_URL and the matching Docker auth headers."
1889
+ "Suggestion: docker-stack could not read the manager's /version endpoint. Check that DOCKER_MANAGER_URL points "
1890
+ "at a reachable manager and that the Docker auth headers are set - in CI that means running the docker-stack "
1891
+ "setup action before this step."
1811
1892
  )
1812
1893
  if "docker-manager is configured" in lowered and "did not advertise required feature" in lowered:
1813
1894
  return (
@@ -1821,7 +1902,7 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
1821
1902
  output = _error_output(exc)
1822
1903
  lines = [f"docker-stack: command failed with exit code {exc.returncode}: {_format_command(exc.cmd)}"]
1823
1904
  if output:
1824
- lines.append(output)
1905
+ lines.append(_humanize_manager_error(output))
1825
1906
  suggestion = _manager_deploy_suggestion(output)
1826
1907
  if suggestion:
1827
1908
  lines.extend(["", suggestion])
@@ -1830,14 +1911,14 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
1830
1911
 
1831
1912
  def _format_runtime_error(exc: RuntimeError) -> str:
1832
1913
  message = str(exc).strip() or exc.__class__.__name__
1833
- lines = [f"docker-stack: {message}"]
1914
+ lines = [f"docker-stack: {_humanize_manager_error(message)}"]
1834
1915
  suggestion = _manager_deploy_suggestion(message)
1835
1916
  if suggestion:
1836
1917
  lines.extend(["", suggestion])
1837
1918
  return "\n".join(lines)
1838
1919
 
1839
1920
 
1840
- def main(args: List[str] = None):
1921
+ def _run(args: List[str] = None):
1841
1922
  parser = argparse.ArgumentParser(description="Deploy and manage Docker stacks.")
1842
1923
  subparsers = parser.add_subparsers(dest="command", required=True)
1843
1924
 
@@ -2182,5 +2263,18 @@ def main(args: List[str] = None):
2182
2263
  sys.exit(2)
2183
2264
 
2184
2265
 
2266
+ def main(args: List[str] = None):
2267
+ """Entry point. Ctrl+C has to look like an interrupted shell command, not a Python crash.
2268
+
2269
+ Commands that exec into another binary never reach this handler; it covers the paths that stay
2270
+ in Python (cluster-rendered ``ps``, login, deploy) and any HTTP call they are blocked on.
2271
+ """
2272
+ try:
2273
+ return _run(args)
2274
+ except KeyboardInterrupt:
2275
+ print(file=sys.stderr)
2276
+ return 130
2277
+
2278
+
2185
2279
  if __name__ == "__main__":
2186
- main()
2280
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.2.2
3
+ Version: 2.2.4
4
4
  Summary: CLI for deploying and managing Docker stacks.
5
5
  Home-page: https://github.com/mesudip/docker-stack
6
6
  Author: Sudip Bhattarai
@@ -1,5 +1,5 @@
1
1
  docker_stack/__init__.py,sha256=qgHdW8ZDBlyk6FzIf8Mld_cJD1SjOfqo2dELg4pK668,880
2
- docker_stack/cli.py,sha256=SyIReri1H3VGZ_07dJ6hfVvodtYSH5t5FA36KzvjyJo,92405
2
+ docker_stack/cli.py,sha256=_IWypxyKZ4reUaB4qb7WpRt24WtUVWLlmrr3ALY8riM,96017
3
3
  docker_stack/command_runner.py,sha256=mNaUAVKtrJbji_5ARVPLKhc92avqllNKrVQj1A6S0dA,1252
4
4
  docker_stack/compose.py,sha256=_fAVesjyW5ecid46sYYhcu02yF1vqknDSCFYflJPDOE,534
5
5
  docker_stack/docker_objects.py,sha256=U6szytv5gKrmBYKqThSeMMXdiJ2u4STIQLkeqQ-Fn4Q,12103
@@ -13,8 +13,8 @@ docker_stack/merge_conf.py,sha256=Pmsabcgf3SDyaWvf2OLsBPodzEqsvOIjj9xzn_qWXH0,19
13
13
  docker_stack/registry.py,sha256=sWC1J9JDIrcYyhei1POYCZHJ0xUCKC2pveisSHMgYsQ,9036
14
14
  docker_stack/shell_auth.py,sha256=UUu9y4wMFKfc_j86yGcasvS_TLZ2uV4kv0T4XKTMQBQ,33424
15
15
  docker_stack/url_parser.py,sha256=Sk8GQE0nEiwCkijp1OltP2AfgtKEmM2hybcH_rBhfiI,6824
16
- docker_stack-2.2.2.dist-info/METADATA,sha256=NlKnxEWXo3HPuUOTTxzawm3xhYePJPDK3pgfQmK3u6c,13821
17
- docker_stack-2.2.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
18
- docker_stack-2.2.2.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
19
- docker_stack-2.2.2.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
20
- docker_stack-2.2.2.dist-info/RECORD,,
16
+ docker_stack-2.2.4.dist-info/METADATA,sha256=__Y1aknQuctP1Nuf2zhuinUnlyYDu-qGY54rUma8alM,13821
17
+ docker_stack-2.2.4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
18
+ docker_stack-2.2.4.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
19
+ docker_stack-2.2.4.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
20
+ docker_stack-2.2.4.dist-info/RECORD,,