docker-stack 2.2.3__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
@@ -350,6 +350,38 @@ def _print_cluster_containers(payload: Dict[str, object], *, no_trunc: bool = Fa
350
350
  return 1 if errors else 0
351
351
 
352
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
+
353
385
  def _managed_docker(arguments: List[str]) -> int:
354
386
  values = list(arguments)
355
387
  if values and values[0] == "--":
@@ -378,11 +410,11 @@ def _managed_docker(arguments: List[str]) -> int:
378
410
  value in {"-q", "--quiet", "--format"} or value.startswith("--format=")
379
411
  for value in command
380
412
  ):
381
- return subprocess.run(["docker", *values], check=False).returncode
413
+ return _exec_docker(values)
382
414
  try:
383
415
  client = discover_manager_client()
384
416
  if client is None or not client.supports(FEATURE_CLUSTER_CONTAINER_CLI):
385
- return subprocess.run(["docker", *values], check=False).returncode
417
+ return _exec_docker(values)
386
418
  filters = []
387
419
  all_containers = False
388
420
  latest = False
@@ -411,7 +443,7 @@ def _managed_docker(arguments: List[str]) -> int:
411
443
  elif value.startswith("--last="):
412
444
  limit = int(value.split("=", 1)[1])
413
445
  else:
414
- return subprocess.run(["docker", *values], check=False).returncode
446
+ return _exec_docker(values)
415
447
  index += 1
416
448
  payload = client.list_containers(
417
449
  all_containers=all_containers,
@@ -1774,7 +1806,7 @@ def open_context_shell(config_dir: Path, context_name: str) -> int:
1774
1806
  env["DOCKER_CONTEXT"] = context_name
1775
1807
  env["DOCKER_MANAGER_CONTEXT_NAME"] = context_name
1776
1808
  shell = env.get("SHELL", "").strip() or "/bin/bash"
1777
- return subprocess.run([shell, "-i"], check=False, env=env).returncode
1809
+ return _exec_process([shell, "-i"], env=env)
1778
1810
 
1779
1811
 
1780
1812
  def active_shell_config_dir(context_name: str) -> Optional[Path]:
@@ -1805,6 +1837,29 @@ def _error_output(exc: subprocess.CalledProcessError) -> str:
1805
1837
  return "\n".join(value for value in values if value)
1806
1838
 
1807
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
+
1808
1863
  def _manager_deploy_suggestion(message: str) -> Optional[str]:
1809
1864
  if GITHUB_WORKFLOW_RESTRICTED_MESSAGE in message:
1810
1865
  return (
@@ -1824,10 +1879,16 @@ def _manager_deploy_suggestion(message: str) -> Optional[str]:
1824
1879
  "by this stack. Either allow that external network in the Docker-Manager deployment rule, attach the service to "
1825
1880
  "a stack-owned network, or relabel/recreate the existing network with the expected stack ownership before deploying."
1826
1881
  )
1827
- 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):
1828
1888
  return (
1829
- "Suggestion: check that the manager URL is reachable from the runner and that the setup action exported "
1830
- "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."
1831
1892
  )
1832
1893
  if "docker-manager is configured" in lowered and "did not advertise required feature" in lowered:
1833
1894
  return (
@@ -1841,7 +1902,7 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
1841
1902
  output = _error_output(exc)
1842
1903
  lines = [f"docker-stack: command failed with exit code {exc.returncode}: {_format_command(exc.cmd)}"]
1843
1904
  if output:
1844
- lines.append(output)
1905
+ lines.append(_humanize_manager_error(output))
1845
1906
  suggestion = _manager_deploy_suggestion(output)
1846
1907
  if suggestion:
1847
1908
  lines.extend(["", suggestion])
@@ -1850,14 +1911,14 @@ def _format_called_process_error(exc: subprocess.CalledProcessError) -> str:
1850
1911
 
1851
1912
  def _format_runtime_error(exc: RuntimeError) -> str:
1852
1913
  message = str(exc).strip() or exc.__class__.__name__
1853
- lines = [f"docker-stack: {message}"]
1914
+ lines = [f"docker-stack: {_humanize_manager_error(message)}"]
1854
1915
  suggestion = _manager_deploy_suggestion(message)
1855
1916
  if suggestion:
1856
1917
  lines.extend(["", suggestion])
1857
1918
  return "\n".join(lines)
1858
1919
 
1859
1920
 
1860
- def main(args: List[str] = None):
1921
+ def _run(args: List[str] = None):
1861
1922
  parser = argparse.ArgumentParser(description="Deploy and manage Docker stacks.")
1862
1923
  subparsers = parser.add_subparsers(dest="command", required=True)
1863
1924
 
@@ -2202,5 +2263,18 @@ def main(args: List[str] = None):
2202
2263
  sys.exit(2)
2203
2264
 
2204
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
+
2205
2279
  if __name__ == "__main__":
2206
- main()
2280
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.2.3
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=K4hjxK7Bw9KOLXgyR1PGfXkmpMsNHKE4U1TUP3pmlzI,92958
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.3.dist-info/METADATA,sha256=JcGdwt8N9ijokqp2cTdXMPTYjgCjzqVM_SYlH6fmWjM,13821
17
- docker_stack-2.2.3.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
18
- docker_stack-2.2.3.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
19
- docker_stack-2.2.3.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
20
- docker_stack-2.2.3.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,,