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 +105 -11
- {docker_stack-2.2.2.dist-info → docker_stack-2.2.4.dist-info}/METADATA +1 -1
- {docker_stack-2.2.2.dist-info → docker_stack-2.2.4.dist-info}/RECORD +6 -6
- {docker_stack-2.2.2.dist-info → docker_stack-2.2.4.dist-info}/WHEEL +0 -0
- {docker_stack-2.2.2.dist-info → docker_stack-2.2.4.dist-info}/entry_points.txt +0 -0
- {docker_stack-2.2.2.dist-info → docker_stack-2.2.4.dist-info}/top_level.txt +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
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:
|
|
1810
|
-
"
|
|
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
|
|
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,5 +1,5 @@
|
|
|
1
1
|
docker_stack/__init__.py,sha256=qgHdW8ZDBlyk6FzIf8Mld_cJD1SjOfqo2dELg4pK668,880
|
|
2
|
-
docker_stack/cli.py,sha256=
|
|
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.
|
|
17
|
-
docker_stack-2.2.
|
|
18
|
-
docker_stack-2.2.
|
|
19
|
-
docker_stack-2.2.
|
|
20
|
-
docker_stack-2.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|