11import asyncio
22import time
33
4- import docker . errors
4+ import docker
55from docker import DockerClient
66from typing import Callable , Any
77from off_key_core .config .logs import logger , log_performance
88from ..config .config import get_tactic_settings
99
1010
11+ _SWARM_FALLBACK_INDICATORS = (
12+ "cannot be used with services" ,
13+ "only networks scoped to the swarm can be used" ,
14+ "this node is not a swarm manager" ,
15+ "swarm mode" ,
16+ )
17+
18+
19+ def _extract_latest_workload_state (
20+ tasks : list [object ], no_tasks_state : str = "no_tasks"
21+ ) -> str :
22+ task_items = [task for task in tasks if isinstance (task , dict )]
23+ if not task_items :
24+ return no_tasks_state
25+ latest = max (task_items , key = lambda task : str (task .get ("CreatedAt" , "" )))
26+ status = latest .get ("Status" )
27+ if not isinstance (status , dict ):
28+ status = {}
29+ return str (status .get ("State" , "unknown" )).strip ().lower ()
30+
31+
1132class AsyncDocker :
1233 def __init__ (self , docker_config = None ):
1334 config = docker_config or get_tactic_settings ().config .docker
@@ -52,19 +73,11 @@ def close(self) -> None:
5273 logger .warning (f"Error closing Docker client: { e } " )
5374
5475
55- def _should_fallback_to_container (exc : Exception ) -> bool :
76+ def should_fallback_to_container (exc : Exception ) -> bool :
5677 if isinstance (exc , docker .errors .APIError ) and exc .status_code in (400 , 406 , 503 ):
5778 return True
5879 text = str (exc ).lower ()
59- return any (
60- indicator in text
61- for indicator in (
62- "cannot be used with services" ,
63- "only networks scoped to the swarm can be used" ,
64- "this node is not a swarm manager" ,
65- "swarm mode is not active" ,
66- )
67- )
80+ return any (indicator in text for indicator in _SWARM_FALLBACK_INDICATORS )
6881
6982
7083async def get_workload_docker_status (
@@ -73,9 +86,9 @@ async def get_workload_docker_status(
7386 """Return the running status of a Docker workload by ID.
7487
7588 Tries Swarm services first (reads task state), then falls back to plain
76- containers (reads container status). Both callers — RadarOrchestrationService
77- and RadarStatusReconciliationService — delegate here to avoid duplicating the
78- service→ container probe-and-fallback logic.
89+ containers (reads container status). Both callers -- RadarOrchestrationService
90+ and RadarStatusReconciliationService -- delegate here to avoid duplicating the
91+ service-> container probe-and-fallback logic.
7992
8093 Returns:
8194 "running" | task state | "no_tasks" | "not_found" | "no_container_id"
@@ -89,14 +102,11 @@ async def get_workload_docker_status(
89102 async_docker .client .services .get , container_id
90103 )
91104 tasks = await async_docker .run (docker_service .tasks )
92- if tasks :
93- latest = max (tasks , key = lambda t : t .get ("CreatedAt" , "" ))
94- return latest .get ("Status" , {}).get ("State" , "unknown" )
95- return "no_tasks"
105+ return _extract_latest_workload_state (tasks )
96106 except docker .errors .NotFound :
97107 pass
98108 except Exception as exc :
99- if not _should_fallback_to_container (exc ):
109+ if not should_fallback_to_container (exc ):
100110 raise
101111 logger .debug (
102112 "Skipping Swarm service status lookup for workload %s: %s" ,
@@ -114,3 +124,32 @@ async def get_workload_docker_status(
114124 except Exception as exc :
115125 logger .debug ("Error checking Docker status for %s: %s" , container_id , exc )
116126 return "error"
127+
128+
129+ async def with_workload_fallback (
130+ async_docker : AsyncDocker ,
131+ container_id : str ,
132+ on_service : Callable [[Any ], Any ],
133+ on_container : Callable [[Any ], Any ],
134+ ) -> Any :
135+ """Resolve a Docker workload as swarm service first, then fallback to container."""
136+ try :
137+ docker_service = await async_docker .run (
138+ async_docker .client .services .get , container_id
139+ )
140+ return await async_docker .run (on_service , docker_service )
141+ except docker .errors .NotFound :
142+ pass
143+ except Exception as exc :
144+ if not should_fallback_to_container (exc ):
145+ raise
146+ logger .debug (
147+ "Skipping Swarm service operation for workload %s: %s" ,
148+ container_id ,
149+ exc ,
150+ )
151+
152+ docker_container = await async_docker .run (
153+ async_docker .client .containers .get , container_id
154+ )
155+ return await async_docker .run (on_container , docker_container )
0 commit comments