docker-stack 2.0.0__tar.gz → 2.0.2__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.
Files changed (31) hide show
  1. {docker_stack-2.0.0 → docker_stack-2.0.2}/PKG-INFO +1 -1
  2. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/cli.py +7 -4
  3. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/manager_api.py +30 -69
  4. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/PKG-INFO +1 -1
  5. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/SOURCES.txt +1 -0
  6. {docker_stack-2.0.0 → docker_stack-2.0.2}/setup.py +1 -1
  7. {docker_stack-2.0.0 → docker_stack-2.0.2}/tests/test_docker_stack.py +4 -1
  8. {docker_stack-2.0.0 → docker_stack-2.0.2}/tests/test_login.py +1 -1
  9. docker_stack-2.0.2/tests/test_manager_api.py +72 -0
  10. {docker_stack-2.0.0 → docker_stack-2.0.2}/README.md +0 -0
  11. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/__init__.py +0 -0
  12. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/command_runner.py +0 -0
  13. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/compose.py +0 -0
  14. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/docker_objects.py +0 -0
  15. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/envsubst.py +0 -0
  16. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/envsubst_merge.py +0 -0
  17. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/helpers.py +0 -0
  18. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/login.py +0 -0
  19. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/markers.py +0 -0
  20. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/merge_conf.py +0 -0
  21. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/registry.py +0 -0
  22. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack/url_parser.py +0 -0
  23. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/dependency_links.txt +0 -0
  24. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/entry_points.txt +0 -0
  25. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/requires.txt +0 -0
  26. {docker_stack-2.0.0 → docker_stack-2.0.2}/docker_stack.egg-info/top_level.txt +0 -0
  27. {docker_stack-2.0.0 → docker_stack-2.0.2}/pyproject.toml +0 -0
  28. {docker_stack-2.0.0 → docker_stack-2.0.2}/setup.cfg +0 -0
  29. {docker_stack-2.0.0 → docker_stack-2.0.2}/tests/test_docker_objects.py +0 -0
  30. {docker_stack-2.0.0 → docker_stack-2.0.2}/tests/test_load_env.py +0 -0
  31. {docker_stack-2.0.0 → docker_stack-2.0.2}/tests/test_node_ls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -639,10 +639,9 @@ class DockerStack:
639
639
  )
640
640
  return
641
641
 
642
- _, cmd = self.docker.config.increment(stack_name, rendered_content, labels=labels, stack=stack_name)
643
- if not cmd.isNop():
644
- self.commands.append(cmd)
645
-
642
+ # Manager-backed deploys must stay on the manager stack APIs. Hitting
643
+ # direct daemon config endpoints here breaks GitHub OIDC workflows,
644
+ # which are intentionally restricted away from generic daemon access.
646
645
  if manager_deploy and not with_registry_auth:
647
646
  self.commands.append(
648
647
  CallbackCommand(
@@ -657,6 +656,10 @@ class DockerStack:
657
656
  )
658
657
  return
659
658
 
659
+ _, cmd = self.docker.config.increment(stack_name, rendered_content, labels=labels, stack=stack_name)
660
+ if not cmd.isNop():
661
+ self.commands.append(cmd)
662
+
660
663
  cmd = ["docker", "stack", "deploy", "-c", str(rendered_filename), stack_name]
661
664
  if with_registry_auth:
662
665
  cmd.insert(3, "--with-registry-auth")
@@ -48,6 +48,28 @@ def _manager_target_from_env() -> Optional[str]:
48
48
  return None
49
49
 
50
50
 
51
+ def _format_control_plane_endpoints(endpoints: Any) -> str:
52
+ if not isinstance(endpoints, list) or not endpoints:
53
+ return "none"
54
+ values = []
55
+ for item in endpoints:
56
+ if not isinstance(item, dict):
57
+ continue
58
+ endpoint_id = item.get("id")
59
+ name = str(item.get("name") or "").strip()
60
+ slug = str(item.get("slug") or "").strip()
61
+ parts = []
62
+ if endpoint_id is not None:
63
+ parts.append(f"id={endpoint_id}")
64
+ if name:
65
+ parts.append(f"name={name}")
66
+ if slug:
67
+ parts.append(f"slug={slug}")
68
+ if parts:
69
+ values.append(", ".join(parts))
70
+ return "; ".join(values) if values else "none"
71
+
72
+
51
73
  class ManagerApiClient:
52
74
  def __init__(
53
75
  self,
@@ -134,70 +156,13 @@ class ManagerApiClient:
134
156
 
135
157
  self._endpoint_id_checked = True
136
158
 
137
- env_endpoint_id = os.getenv("DOCKER_MANAGER_ENDPOINT_ID", "").strip()
138
- if env_endpoint_id:
139
- try:
140
- parsed_id = int(env_endpoint_id)
141
- except ValueError as exc:
142
- raise RuntimeError("DOCKER_MANAGER_ENDPOINT_ID must be a positive integer") from exc
143
- if parsed_id <= 0:
144
- raise RuntimeError("DOCKER_MANAGER_ENDPOINT_ID must be a positive integer")
145
- self._endpoint_id = parsed_id
146
- return parsed_id
147
-
148
159
  payload = self._request_json("/api/endpoints")
149
160
  endpoints = payload.get("endpoints")
150
- if not isinstance(endpoints, list) or not endpoints:
151
- raise RuntimeError("No visible Docker-Manager endpoints available")
152
-
153
- endpoint_slug = os.getenv("DOCKER_MANAGER_ENDPOINT_SLUG", "").strip()
154
- endpoint_name = os.getenv("DOCKER_MANAGER_ENDPOINT_NAME", "").strip()
155
- selected = None
156
-
157
- if endpoint_slug:
158
- selected = next(
159
- (
160
- item
161
- for item in endpoints
162
- if isinstance(item, dict) and str(item.get("slug", "")).strip() == endpoint_slug
163
- ),
164
- None,
165
- )
166
- if selected is None:
167
- raise RuntimeError(
168
- f"Could not find endpoint with slug '{endpoint_slug}'. "
169
- "Set DOCKER_MANAGER_ENDPOINT_ID to select explicitly."
170
- )
171
- elif endpoint_name:
172
- selected = next(
173
- (
174
- item
175
- for item in endpoints
176
- if isinstance(item, dict) and str(item.get("name", "")).strip() == endpoint_name
177
- ),
178
- None,
179
- )
180
- if selected is None:
181
- raise RuntimeError(
182
- f"Could not find endpoint with name '{endpoint_name}'. "
183
- "Set DOCKER_MANAGER_ENDPOINT_ID to select explicitly."
184
- )
185
- else:
186
- selected = next((item for item in endpoints if isinstance(item, dict)), None)
187
-
188
- if not isinstance(selected, dict):
189
- raise RuntimeError("Docker-Manager endpoint payload is invalid")
190
-
191
- try:
192
- endpoint_id = int(selected.get("id"))
193
- except (TypeError, ValueError) as exc:
194
- raise RuntimeError("Docker-Manager endpoint payload is missing a valid id") from exc
195
-
196
- if endpoint_id <= 0:
197
- raise RuntimeError("Docker-Manager endpoint id must be positive")
198
-
199
- self._endpoint_id = endpoint_id
200
- return endpoint_id
161
+ raise RuntimeError(
162
+ "docker-stack does not support Docker-Manager control-plane targets. "
163
+ "Point DOCKER_MANAGER_URL at a direct manager stack API instead. "
164
+ f"Visible endpoints: {_format_control_plane_endpoints(endpoints)}"
165
+ )
201
166
 
202
167
  def _endpoint_path(self, suffix: str) -> str:
203
168
  normalized = suffix if suffix.startswith("/") else f"/{suffix}"
@@ -208,13 +173,9 @@ class ManagerApiClient:
208
173
  features = self.detect_features()
209
174
  if feature_name not in features:
210
175
  return False
211
- if feature_name in {FEATURE_STACK_QUERY, FEATURE_STACK_DEPLOY}:
176
+ if feature_name == FEATURE_STACK_DEPLOY:
212
177
  if not self._detect_manager_backend():
213
178
  return False
214
- try:
215
- self._resolve_endpoint_id()
216
- except RuntimeError:
217
- return False
218
179
  return True
219
180
 
220
181
  def list_stacks(self) -> Dict[str, Any]:
@@ -300,7 +261,7 @@ class ManagerApiClient:
300
261
  if options:
301
262
  payload["options"] = options
302
263
  return self._request_json(
303
- self._endpoint_path("/stacks/validate"),
264
+ "/api/stacks/validate",
304
265
  method="POST",
305
266
  payload=payload,
306
267
  )
@@ -317,7 +278,7 @@ class ManagerApiClient:
317
278
  if options:
318
279
  payload["options"] = options
319
280
  return self._request_json(
320
- self._endpoint_path("/stacks/deploy"),
281
+ "/api/stacks/deploy",
321
282
  method="POST",
322
283
  payload=payload,
323
284
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -25,4 +25,5 @@ tests/test_docker_objects.py
25
25
  tests/test_docker_stack.py
26
26
  tests/test_load_env.py
27
27
  tests/test_login.py
28
+ tests/test_manager_api.py
28
29
  tests/test_node_ls.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="docker-stack",
5
- version="2.0.0",
5
+ version="2.0.2",
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",
@@ -311,7 +311,10 @@ def test_checkout_uses_manager_fast_path_for_tag(monkeypatch):
311
311
  def test_deploy_enqueues_manager_callback_when_supported(monkeypatch, tmp_path):
312
312
  fake_manager = FakeManagerClient()
313
313
  monkeypatch.setattr("docker_stack.cli.discover_manager_client", lambda *_args, **_kwargs: fake_manager)
314
- monkeypatch.setattr("docker_stack.cli.run_cli_command", lambda *args, **kwargs: "")
314
+ monkeypatch.setattr(
315
+ "docker_stack.cli.run_cli_command",
316
+ lambda *args, **kwargs: pytest.fail("manager-backed deploy should not hit direct daemon CLI"),
317
+ )
315
318
  compose_file = tmp_path / "docker-compose.yml"
316
319
  compose_file.write_text("services:\n api:\n image: busybox\n")
317
320
 
@@ -552,6 +552,6 @@ def test_browser_login_handles_callback_and_token_exchange(monkeypatch):
552
552
  result = browser_login(config, browser_opener=fake_browser_open, port_finder=port_finder)
553
553
 
554
554
  assert result.redirect_uri.endswith("/auth/callback")
555
- assert result.callback_port in range(8070, 8080)
555
+ assert result.callback_port > 0
556
556
  assert result.access_token == "header.eyJleHAiOjE5MDAwMDAwMDB9.signature"
557
557
  assert result.expires_at == 1900000000
@@ -0,0 +1,72 @@
1
+ import pytest
2
+
3
+ from docker_stack.manager_api import FEATURE_STACK_DEPLOY, ManagerApiClient
4
+
5
+
6
+ def test_supports_stack_deploy_without_endpoint_catalog(monkeypatch):
7
+ client = ManagerApiClient("https://172.31.0.6:2378", skip_tls_verify=True)
8
+ calls = []
9
+
10
+ def fake_request(path, *, method="GET", payload=None):
11
+ calls.append((method, path, payload))
12
+ if path == "/version":
13
+ return {"MesudipFeatures": [FEATURE_STACK_DEPLOY]}
14
+ raise AssertionError(f"unexpected request: {method} {path}")
15
+
16
+ monkeypatch.setattr(client, "_request_json", fake_request)
17
+
18
+ assert client.supports(FEATURE_STACK_DEPLOY) is True
19
+ assert calls == [("GET", "/version", None)]
20
+
21
+
22
+ def test_deploy_stack_uses_direct_stack_api(monkeypatch):
23
+ client = ManagerApiClient("https://172.31.0.6:2378", skip_tls_verify=True)
24
+ calls = []
25
+
26
+ def fake_request(path, *, method="GET", payload=None):
27
+ calls.append((method, path, payload))
28
+ return {"warnings": []}
29
+
30
+ monkeypatch.setattr(client, "_request_json", fake_request)
31
+
32
+ payload = client.deploy_stack(
33
+ stack="trusted-publish-test",
34
+ namespace="default",
35
+ compose="services: {}",
36
+ options={},
37
+ )
38
+
39
+ assert payload == {"warnings": []}
40
+ assert calls == [
41
+ (
42
+ "POST",
43
+ "/api/stacks/deploy",
44
+ {
45
+ "stack": "trusted-publish-test",
46
+ "namespace": "default",
47
+ "compose": "services: {}",
48
+ },
49
+ )
50
+ ]
51
+
52
+
53
+ def test_control_plane_error_lists_visible_endpoints(monkeypatch):
54
+ client = ManagerApiClient("https://172.31.0.6:2378", skip_tls_verify=True)
55
+
56
+ def fake_request(path, *, method="GET", payload=None):
57
+ assert path == "/api/endpoints"
58
+ return {
59
+ "endpoints": [
60
+ {"id": 1, "name": "office", "slug": "office"},
61
+ {"id": 2, "name": "lab"},
62
+ ]
63
+ }
64
+
65
+ monkeypatch.setattr(client, "_request_json", fake_request)
66
+
67
+ with pytest.raises(RuntimeError) as excinfo:
68
+ client._resolve_endpoint_id()
69
+
70
+ assert "control-plane targets" in str(excinfo.value)
71
+ assert "id=1, name=office, slug=office" in str(excinfo.value)
72
+ assert "id=2, name=lab" in str(excinfo.value)
File without changes
File without changes