docker-stack 2.2.4__py3-none-any.whl → 2.2.5__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
@@ -37,7 +37,6 @@ from docker_stack.shell_auth import (
37
37
  shell_session_active,
38
38
  )
39
39
  from docker_stack.manager_api import (
40
- FEATURE_CLUSTER_CONTAINER_CLI,
41
40
  FEATURE_STACK_DEPLOY,
42
41
  FEATURE_STACK_QUERY,
43
42
  ManagerApiClient,
@@ -102,15 +101,23 @@ def _write_selected_node(value: Optional[str], display_name: str) -> None:
102
101
  Path(node_state).write_text(f"{display_name}\n", encoding="utf-8")
103
102
 
104
103
 
105
- def _manager_with_cluster_cli() -> ManagerApiClient:
106
- client = discover_manager_client()
107
- if client is None or not client.supports(FEATURE_CLUSTER_CONTAINER_CLI):
108
- raise RuntimeError("Docker-Manager does not support cluster container CLI; upgrade the manager and all node agents")
104
+ def _require_manager() -> ManagerApiClient:
105
+ """Return a client for the Docker-Manager behind this shell, or explain why not.
106
+
107
+ Only the current manager is supported, so there is no capability negotiation here -
108
+ either the target is a Docker-Manager or it is a plain Docker daemon.
109
+ """
110
+ client = discover_manager_client(strict=True)
111
+ if client is None or not client.is_manager_backend():
112
+ raise RuntimeError(
113
+ f"{client.manager_url if client else 'the current Docker endpoint'} is not a "
114
+ "Docker-Manager; node-local commands need one"
115
+ )
109
116
  return client
110
117
 
111
118
 
112
119
  def _select_node(value: str) -> str:
113
- client = _manager_with_cluster_cli()
120
+ client = _require_manager()
114
121
  requested = value.strip()
115
122
  if requested.lower() == "cluster":
116
123
  _write_selected_node(None, "cluster")
@@ -133,6 +140,9 @@ def _select_node(value: str) -> str:
133
140
  raise RuntimeError(f"node '{requested}' is not ready")
134
141
  node_id = str(node.get("id") or "").strip()
135
142
  hostname = str(node.get("hostname") or node_id).strip()
143
+ # Only this node has to be usable. The health of every other node - drained, down, or
144
+ # missing its agent - is none of this command's business.
145
+ client.check_node_agent(node_id or hostname)
136
146
  _write_selected_node(node_id, hostname)
137
147
  return hostname
138
148
 
@@ -339,6 +349,13 @@ def _print_cluster_containers(payload: Dict[str, object], *, no_trunc: bool = Fa
339
349
  columns.append("SIZE")
340
350
  columns.append("NODE")
341
351
  _print_table(columns, rows)
352
+ skipped = payload.get("skipped_nodes") if isinstance(payload.get("skipped_nodes"), list) else []
353
+ for node in skipped:
354
+ if isinstance(node, dict):
355
+ print(
356
+ f"docker ps: skipped node {node.get('node_name') or node.get('node_id')}: {node.get('reason')}",
357
+ file=sys.stderr,
358
+ )
342
359
  errors = payload.get("node_errors") if isinstance(payload.get("node_errors"), list) else []
343
360
  for error in errors:
344
361
  if isinstance(error, dict):
@@ -413,7 +430,7 @@ def _managed_docker(arguments: List[str]) -> int:
413
430
  return _exec_docker(values)
414
431
  try:
415
432
  client = discover_manager_client()
416
- if client is None or not client.supports(FEATURE_CLUSTER_CONTAINER_CLI):
433
+ if client is None or not client.is_manager_backend():
417
434
  return _exec_docker(values)
418
435
  filters = []
419
436
  all_containers = False
@@ -416,6 +416,22 @@ class ManagerApiClient:
416
416
  endpoint_id = self._resolve_endpoint_id()
417
417
  return f"/api/endpoints/{endpoint_id}{normalized}"
418
418
 
419
+ def is_manager_backend(self) -> bool:
420
+ """True when the target is a Docker-Manager stack API rather than a raw daemon."""
421
+ return self._detect_manager_backend()
422
+
423
+ def check_node_agent(self, selector: str) -> Dict[str, Any]:
424
+ """Report whether one node can serve node-local Docker commands.
425
+
426
+ Scoped to ``selector`` on purpose: an unrelated node that is drained, down, or
427
+ missing its agent must never make a usable node look unusable.
428
+ """
429
+ node = urllib.parse.quote(selector, safe="")
430
+ payload = self._request_json(f"/api/docker-stack/nodes/{node}/agent")
431
+ if not isinstance(payload, dict):
432
+ raise RuntimeError("Docker-Manager node agent response is invalid")
433
+ return payload
434
+
419
435
  def supports(self, feature_name: str) -> bool:
420
436
  features = self.detect_features()
421
437
  if FEATURE_MESUDIP_DOCKER_ENTERPRISE in features and feature_name in {
@@ -721,7 +737,15 @@ class ManagerApiClient:
721
737
  )
722
738
 
723
739
 
724
- def discover_manager_client(timeout_secs: int = 5) -> Optional[ManagerApiClient]:
740
+ def discover_manager_client(
741
+ timeout_secs: int = 5, *, strict: bool = False
742
+ ) -> Optional[ManagerApiClient]:
743
+ """Build a client for the manager behind DOCKER_MANAGER_URL or the Docker context.
744
+
745
+ ``strict`` raises the concrete reason instead of returning ``None``. Callers that
746
+ fall back to the plain Docker CLI want the quiet form; callers that are about to
747
+ report a failure to the user must not discard why discovery failed.
748
+ """
725
749
  try:
726
750
  target = _manager_target_from_env()
727
751
  if target:
@@ -729,11 +753,26 @@ def discover_manager_client(timeout_secs: int = 5) -> Optional[ManagerApiClient]
729
753
  elif shutil.which("docker"):
730
754
  _, context_target = current_docker_context_target()
731
755
  if not context_target or not context_target.startswith(("tcp://", "http://", "https://")):
756
+ if strict:
757
+ raise RuntimeError(
758
+ "no Docker-Manager endpoint found: set DOCKER_MANAGER_URL, or select a "
759
+ f"docker context with a tcp:// endpoint (current target: {context_target or 'none'})"
760
+ )
732
761
  return None
733
762
  config = resolve_login_config(manager_target=context_target)
734
763
  else:
764
+ if strict:
765
+ raise RuntimeError(
766
+ "no Docker-Manager endpoint found: DOCKER_MANAGER_URL is unset and the docker CLI is not installed"
767
+ )
735
768
  return None
736
- except Exception:
769
+ except RuntimeError:
770
+ if strict:
771
+ raise
772
+ return None
773
+ except Exception as exc:
774
+ if strict:
775
+ raise RuntimeError(f"failed resolving the Docker-Manager endpoint: {exc}") from exc
737
776
  return None
738
777
 
739
778
  return ManagerApiClient(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.2.4
3
+ Version: 2.2.5
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=_IWypxyKZ4reUaB4qb7WpRt24WtUVWLlmrr3ALY8riM,96017
2
+ docker_stack/cli.py,sha256=NlUsiSLLrP7drUP5FCj8bwdc32ZFe6d2wHP_tQMnFk0,96797
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
@@ -7,14 +7,14 @@ docker_stack/envsubst.py,sha256=gdEUpsjWJeNbbahySk56vUdQh1nns85o3LbU6SUBo3I,8337
7
7
  docker_stack/envsubst_merge.py,sha256=TwOu8BducG0rWnSreJGwesNi-VghCetljcf-aWceE8M,4971
8
8
  docker_stack/helpers.py,sha256=nJHaIS1vtFyPYclXVl3-Qd-1h55jnSy1LwdATDrF4hk,5953
9
9
  docker_stack/login.py,sha256=jnrmrDh79Brlzj89YdZ1sGpg-ex16yb3ZN47eFpcW5g,41754
10
- docker_stack/manager_api.py,sha256=xb8RwY4LEn1usg9qhG9AfiCvBJ2tpQawVMLRwF3vYHY,28361
10
+ docker_stack/manager_api.py,sha256=ZTHcudzHuLq0dDN-qu51nJyX0hQy5PEPHvf807y3jyw,30194
11
11
  docker_stack/markers.py,sha256=gqStnr0tNWZ1kzqvREc2jVQVQrevheShgJtL5QzSx4g,2388
12
12
  docker_stack/merge_conf.py,sha256=Pmsabcgf3SDyaWvf2OLsBPodzEqsvOIjj9xzn_qWXH0,1917
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.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,,
16
+ docker_stack-2.2.5.dist-info/METADATA,sha256=4cqUzfi3pZrTNSL1oqrf9xW0RN4ijdVvSXZAbULFqwY,13821
17
+ docker_stack-2.2.5.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
18
+ docker_stack-2.2.5.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
19
+ docker_stack-2.2.5.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
20
+ docker_stack-2.2.5.dist-info/RECORD,,