docker-stack 2.2.2__tar.gz → 2.2.4__tar.gz
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-2.2.2 → docker_stack-2.2.4}/PKG-INFO +1 -1
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/cli.py +105 -11
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/PKG-INFO +1 -1
- {docker_stack-2.2.2 → docker_stack-2.2.4}/setup.py +1 -1
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_docker_stack.py +136 -11
- {docker_stack-2.2.2 → docker_stack-2.2.4}/README.md +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/__init__.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/command_runner.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/compose.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/docker_objects.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/envsubst.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/envsubst_merge.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/helpers.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/login.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/manager_api.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/markers.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/merge_conf.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/registry.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/shell_auth.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack/url_parser.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/SOURCES.txt +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/dependency_links.txt +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/entry_points.txt +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/requires.txt +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/docker_stack.egg-info/top_level.txt +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/pyproject.toml +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/setup.cfg +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_docker_objects.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_load_env.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_login.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_manager_api.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_node_ls.py +0 -0
- {docker_stack-2.2.2 → docker_stack-2.2.4}/tests/test_shell_auth.py +0 -0
|
@@ -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())
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="docker-stack",
|
|
5
|
-
version="2.2.
|
|
5
|
+
version="2.2.4",
|
|
6
6
|
description="CLI for deploying and managing Docker stacks.",
|
|
7
7
|
long_description=open("README.md").read(), # You can include a README file to describe your package
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -12,6 +12,8 @@ import yaml
|
|
|
12
12
|
from docker_stack.cli import (
|
|
13
13
|
DOCKER_SHELL_ENDPOINT_ENV_VARS,
|
|
14
14
|
Docker,
|
|
15
|
+
_format_runtime_error,
|
|
16
|
+
_exec_process,
|
|
15
17
|
_managed_docker,
|
|
16
18
|
_read_selected_node,
|
|
17
19
|
_select_node,
|
|
@@ -131,6 +133,57 @@ def test_managed_docker_ps_matches_docker_columns(monkeypatch, capsys):
|
|
|
131
133
|
assert row == row.rstrip()
|
|
132
134
|
|
|
133
135
|
|
|
136
|
+
@pytest.mark.parametrize(
|
|
137
|
+
"image,expected,expected_no_trunc",
|
|
138
|
+
[
|
|
139
|
+
(
|
|
140
|
+
"registry.example.com/app/design-system:af037b33@sha256:15d623cfa2f856c8101f9f3c47003d412f9b2bf72c69c95d5dff07a74e5f7b60",
|
|
141
|
+
"registry.example.com/app/design-system:af037b33",
|
|
142
|
+
"registry.example.com/app/design-system:af037b33@sha256:15d623cfa2f856c8101f9f3c47003d412f9b2bf72c69c95d5dff07a74e5f7b60",
|
|
143
|
+
),
|
|
144
|
+
(
|
|
145
|
+
"docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
|
|
146
|
+
"alpine",
|
|
147
|
+
"docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
|
|
148
|
+
),
|
|
149
|
+
("nginx:1.27", "nginx:1.27", "nginx:1.27"),
|
|
150
|
+
(
|
|
151
|
+
"sha256:0e901e68141fd02f237cf63eb842529f8a9500636a9419e3cf4fb986b8fe3d5d",
|
|
152
|
+
"0e901e68141f",
|
|
153
|
+
"sha256:0e901e68141fd02f237cf63eb842529f8a9500636a9419e3cf4fb986b8fe3d5d",
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
def test_managed_docker_ps_drops_pinned_digest_like_docker(monkeypatch, capsys, image, expected, expected_no_trunc):
|
|
158
|
+
client = SimpleNamespace(
|
|
159
|
+
supports=lambda feature: feature == "cluster_container_cli_v1",
|
|
160
|
+
list_containers=lambda **_kwargs: {
|
|
161
|
+
"containers": [
|
|
162
|
+
{
|
|
163
|
+
"node_name": "worker-02",
|
|
164
|
+
"docker": {
|
|
165
|
+
"Id": "abcdef1234567890",
|
|
166
|
+
"Image": image,
|
|
167
|
+
"Command": "sleep",
|
|
168
|
+
"Created": int(time.time()) - 60,
|
|
169
|
+
"Status": "Up 1 minute",
|
|
170
|
+
"Ports": [],
|
|
171
|
+
"Names": ["/svc.1.abc"],
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"node_errors": [],
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
monkeypatch.setattr("docker_stack.cli.discover_manager_client", lambda: client)
|
|
179
|
+
|
|
180
|
+
assert _managed_docker(["ps"]) == 0
|
|
181
|
+
assert capsys.readouterr().out.splitlines()[1].split()[1] == expected
|
|
182
|
+
|
|
183
|
+
assert _managed_docker(["ps", "--no-trunc"]) == 0
|
|
184
|
+
assert capsys.readouterr().out.splitlines()[1].split()[1] == expected_no_trunc
|
|
185
|
+
|
|
186
|
+
|
|
134
187
|
def test_managed_docker_ps_forwards_global_latest_and_limit(monkeypatch):
|
|
135
188
|
calls = []
|
|
136
189
|
client = SimpleNamespace(
|
|
@@ -149,11 +202,65 @@ def test_managed_docker_ps_forwards_global_latest_and_limit(monkeypatch):
|
|
|
149
202
|
assert calls[1]["limit"] == 3
|
|
150
203
|
|
|
151
204
|
|
|
152
|
-
def
|
|
205
|
+
def test_exec_process_replaces_the_python_process(monkeypatch):
|
|
153
206
|
calls = []
|
|
207
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
208
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", lambda file, args: calls.append((file, args)))
|
|
209
|
+
|
|
210
|
+
_exec_process(["docker", "service", "logs", "-f", "svc"])
|
|
211
|
+
|
|
212
|
+
assert calls == [("docker", ["docker", "service", "logs", "-f", "svc"])]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_exec_process_passes_env_when_given(monkeypatch):
|
|
216
|
+
calls = []
|
|
217
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
218
|
+
monkeypatch.setattr("docker_stack.cli.os.execvpe", lambda file, args, env: calls.append((file, args, env)))
|
|
219
|
+
|
|
220
|
+
_exec_process(["/bin/test-shell", "-i"], env={"DOCKER_CONTEXT": "office"})
|
|
221
|
+
|
|
222
|
+
assert calls == [("/bin/test-shell", ["/bin/test-shell", "-i"], {"DOCKER_CONTEXT": "office"})]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_exec_process_reports_missing_binary(monkeypatch, capsys):
|
|
226
|
+
monkeypatch.setattr(os, "name", "posix")
|
|
227
|
+
|
|
228
|
+
def missing(_file, _args):
|
|
229
|
+
raise FileNotFoundError(2, "No such file or directory")
|
|
230
|
+
|
|
231
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", missing)
|
|
232
|
+
|
|
233
|
+
assert _exec_process(["docker", "ps"]) == 127
|
|
234
|
+
assert "cannot execute docker" in capsys.readouterr().err
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_exec_process_falls_back_to_subprocess_off_posix(monkeypatch):
|
|
238
|
+
calls = []
|
|
239
|
+
monkeypatch.setattr(os, "name", "nt")
|
|
240
|
+
monkeypatch.setattr("docker_stack.cli.os.execvp", lambda *_args: pytest.fail("exec must not run off POSIX"))
|
|
154
241
|
monkeypatch.setattr(
|
|
155
242
|
"docker_stack.cli.subprocess.run",
|
|
156
|
-
lambda command, **_kwargs: calls.append(command) or SimpleNamespace(returncode=
|
|
243
|
+
lambda command, **_kwargs: calls.append(command) or SimpleNamespace(returncode=3),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert _exec_process(["docker", "ps"]) == 3
|
|
247
|
+
assert calls == [["docker", "ps"]]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_main_reports_interrupt_as_shell_exit_code(monkeypatch):
|
|
251
|
+
def interrupted(_args):
|
|
252
|
+
raise KeyboardInterrupt
|
|
253
|
+
|
|
254
|
+
monkeypatch.setattr("docker_stack.cli._run", interrupted)
|
|
255
|
+
|
|
256
|
+
assert main(["ps"]) == 130
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_managed_docker_delegates_format_to_real_docker(monkeypatch):
|
|
260
|
+
calls = []
|
|
261
|
+
monkeypatch.setattr(
|
|
262
|
+
"docker_stack.cli._exec_process",
|
|
263
|
+
lambda command, **_kwargs: calls.append(command) or 7,
|
|
157
264
|
)
|
|
158
265
|
|
|
159
266
|
assert _managed_docker(["ps", "--format", "{{.ID}}"]) == 7
|
|
@@ -173,8 +280,8 @@ def test_managed_docker_delegates_format_to_real_docker(monkeypatch):
|
|
|
173
280
|
def test_managed_docker_delegates_explicit_target_overrides(monkeypatch, arguments):
|
|
174
281
|
calls = []
|
|
175
282
|
monkeypatch.setattr(
|
|
176
|
-
"docker_stack.cli.
|
|
177
|
-
lambda command, **_kwargs: calls.append(command) or
|
|
283
|
+
"docker_stack.cli._exec_process",
|
|
284
|
+
lambda command, **_kwargs: calls.append(command) or 6,
|
|
178
285
|
)
|
|
179
286
|
monkeypatch.setattr(
|
|
180
287
|
"docker_stack.cli.discover_manager_client",
|
|
@@ -190,8 +297,8 @@ def test_managed_docker_ps_delegates_when_manager_lacks_cluster_feature(monkeypa
|
|
|
190
297
|
client = SimpleNamespace(supports=lambda _feature: False)
|
|
191
298
|
monkeypatch.setattr("docker_stack.cli.discover_manager_client", lambda: client)
|
|
192
299
|
monkeypatch.setattr(
|
|
193
|
-
"docker_stack.cli.
|
|
194
|
-
lambda command, **_kwargs: calls.append(command) or
|
|
300
|
+
"docker_stack.cli._exec_process",
|
|
301
|
+
lambda command, **_kwargs: calls.append(command) or 9,
|
|
195
302
|
)
|
|
196
303
|
|
|
197
304
|
assert _managed_docker(["ps"]) == 9
|
|
@@ -1426,18 +1533,16 @@ def test_open_context_shell_clears_endpoint_overrides(monkeypatch, tmp_path):
|
|
|
1426
1533
|
monkeypatch.setenv(key, f"parent-{key}")
|
|
1427
1534
|
monkeypatch.setenv("SHELL", "/bin/test-shell")
|
|
1428
1535
|
|
|
1429
|
-
def
|
|
1536
|
+
def fake_exec(cmd, env=None):
|
|
1430
1537
|
captured["cmd"] = cmd
|
|
1431
|
-
captured["check"] = check
|
|
1432
1538
|
captured["env"] = env
|
|
1433
|
-
return
|
|
1539
|
+
return 0
|
|
1434
1540
|
|
|
1435
|
-
monkeypatch.setattr("docker_stack.cli.
|
|
1541
|
+
monkeypatch.setattr("docker_stack.cli._exec_process", fake_exec)
|
|
1436
1542
|
|
|
1437
1543
|
assert open_context_shell(tmp_path, "office") == 0
|
|
1438
1544
|
|
|
1439
1545
|
assert captured["cmd"] == ["/bin/test-shell", "-i"]
|
|
1440
|
-
assert captured["check"] is False
|
|
1441
1546
|
assert captured["env"]["DOCKER_CONFIG"] == str(tmp_path)
|
|
1442
1547
|
assert captured["env"]["DOCKER_CONTEXT"] == "office"
|
|
1443
1548
|
for key in DOCKER_SHELL_ENDPOINT_ENV_VARS:
|
|
@@ -1522,3 +1627,23 @@ def test_context_use_preserves_auth_when_switching_to_manager(monkeypatch, capsy
|
|
|
1522
1627
|
output = capsys.readouterr().out
|
|
1523
1628
|
assert "DOCKER_CONTEXT=office" in output
|
|
1524
1629
|
assert "auth header preserved" in output
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def test_stack_not_found_error_does_not_suggest_ci_setup():
|
|
1633
|
+
formatted = _format_runtime_error(
|
|
1634
|
+
RuntimeError(
|
|
1635
|
+
"Manager request failed (GET /api/docker-stack/stacks/docker-manager/versions?namespace=infra): "
|
|
1636
|
+
'HTTP 404: {"message":"stack not found","incident_id":"d9d372ed","trace_id":"d9d372ed"}'
|
|
1637
|
+
)
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
assert "HTTP 404: stack not found (incident d9d372ed)" in formatted
|
|
1641
|
+
assert '{"message"' not in formatted
|
|
1642
|
+
assert "has no stack by that name in that namespace" in formatted
|
|
1643
|
+
assert "setup action" not in formatted
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def test_version_probe_failure_still_suggests_manager_url_check():
|
|
1647
|
+
formatted = _format_runtime_error(RuntimeError("Manager request failed (GET /version): connection refused"))
|
|
1648
|
+
|
|
1649
|
+
assert "DOCKER_MANAGER_URL points at a reachable manager" in formatted
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|