1010
1111from typing_extensions import override
1212
13- from pyinfra .api import FactBase
13+ from pyinfra .api import FactBase , ShortFactBase
1414
1515
1616class DockerFactBase (FactBase ):
@@ -28,6 +28,18 @@ def process(self, output):
2828 return json .loads (output )
2929
3030
31+ class _DockerJsonLinesFactBase (FactBase ):
32+ abstract = True
33+
34+ @override
35+ def requires_command (self , * args , ** kwargs ) -> str :
36+ return "docker"
37+
38+ @override
39+ def process (self , output ):
40+ return [json .loads (line ) for line in output if line .strip ()]
41+
42+
3143class DockerSystemInfo (DockerFactBase ):
3244 """
3345 Returns ``docker system info`` output in JSON format.
@@ -38,6 +50,24 @@ def command(self) -> str:
3850 return 'docker system info --format="{{json .}}"'
3951
4052
53+ class DockerVersion (FactBase [str ]):
54+ """
55+ Returns the Docker version.
56+ """
57+
58+ @override
59+ def requires_command (self ) -> str :
60+ return "docker"
61+
62+ @override
63+ def command (self ) -> str :
64+ return "docker --version"
65+
66+ @override
67+ def process (self , output ):
68+ return "" .join (output ).replace ("\n " , "" )
69+
70+
4171# All Docker objects
4272#
4373
@@ -149,3 +179,216 @@ class DockerPlugin(DockerSingleMixin):
149179 """
150180
151181 docker_type = "plugin"
182+
183+
184+ # Derived facts: extract specific slices from the full inspect output.
185+ #
186+
187+
188+ class DockerContainerEnvs (ShortFactBase ):
189+ """
190+ Returns environment variables for all Docker containers, keyed by container ID.
191+ """
192+
193+ fact = DockerContainers
194+
195+ @override
196+ def process_data (self , data ):
197+ result = {}
198+ for container in data or []:
199+ container_id = container .get ("Id" )
200+ env_list = (container .get ("Config" ) or {}).get ("Env" ) or []
201+ env : dict = {}
202+ for entry in env_list :
203+ key , _ , value = entry .partition ("=" )
204+ env [key ] = value
205+ result [container_id ] = env
206+ return result
207+
208+
209+ class DockerContainerLabels (ShortFactBase ):
210+ """
211+ Returns labels for all Docker containers, keyed by container ID.
212+ """
213+
214+ fact = DockerContainers
215+
216+ @override
217+ def process_data (self , data ):
218+ return {
219+ container .get ("Id" ): (container .get ("Config" ) or {}).get ("Labels" ) or {}
220+ for container in data or []
221+ }
222+
223+
224+ class DockerContainerMounts (ShortFactBase ):
225+ """
226+ Returns mounts for all Docker containers, keyed by container ID.
227+ """
228+
229+ fact = DockerContainers
230+
231+ @override
232+ def process_data (self , data ):
233+ return {container .get ("Id" ): container .get ("Mounts" ) or [] for container in data or []}
234+
235+
236+ class DockerContainerNetworks (ShortFactBase ):
237+ """
238+ Returns networks for all Docker containers, keyed by container ID.
239+ """
240+
241+ fact = DockerContainers
242+
243+ @override
244+ def process_data (self , data ):
245+ return {
246+ container .get ("Id" ): ((container .get ("NetworkSettings" ) or {}).get ("Networks" ) or {})
247+ for container in data or []
248+ }
249+
250+
251+ class DockerContainerPorts (ShortFactBase ):
252+ """
253+ Returns published port bindings for all Docker containers, keyed by container ID.
254+ """
255+
256+ fact = DockerContainers
257+
258+ @override
259+ def process_data (self , data ):
260+ return {
261+ container .get ("Id" ): ((container .get ("NetworkSettings" ) or {}).get ("Ports" ) or {})
262+ for container in data or []
263+ }
264+
265+
266+ class DockerImageLabels (ShortFactBase ):
267+ """
268+ Returns labels for all Docker images, keyed by image ID.
269+ """
270+
271+ fact = DockerImages
272+
273+ @override
274+ def process_data (self , data ):
275+ return {
276+ image .get ("Id" ): (
277+ (image .get ("Config" ) or {}).get ("Labels" )
278+ or (image .get ("ContainerConfig" ) or {}).get ("Labels" )
279+ or {}
280+ )
281+ for image in data or []
282+ }
283+
284+
285+ class DockerImageLayers (ShortFactBase ):
286+ """
287+ Returns layer digests for all Docker images, keyed by image ID.
288+ """
289+
290+ fact = DockerImages
291+
292+ @override
293+ def process_data (self , data ):
294+ return {
295+ image .get ("Id" ): (image .get ("RootFS" ) or {}).get ("Layers" ) or [] for image in data or []
296+ }
297+
298+
299+ class DockerNetworkLabels (ShortFactBase ):
300+ """
301+ Returns labels for all Docker networks, keyed by network ID.
302+ """
303+
304+ fact = DockerNetworks
305+
306+ @override
307+ def process_data (self , data ):
308+ return {network .get ("Id" ): network .get ("Labels" ) or {} for network in data or []}
309+
310+
311+ class DockerVolumeLabels (ShortFactBase ):
312+ """
313+ Returns labels for all Docker volumes, keyed by volume name.
314+ """
315+
316+ fact = DockerVolumes
317+
318+ @override
319+ def process_data (self , data ):
320+ return {volume .get ("Name" ): volume .get ("Labels" ) or {} for volume in data or []}
321+
322+
323+ # Facts that need their own docker command.
324+ #
325+
326+
327+ class DockerContainerFsChanges (DockerFactBase ):
328+ """
329+ Returns filesystem changes for a single container (``docker diff``) as a list of
330+ ``{"path": ..., "change": "A"|"C"|"D"}`` entries.
331+ """
332+
333+ @override
334+ def command (self , container_id ) -> str :
335+ return "docker container diff {0} 2>&- || true" .format (container_id )
336+
337+ @override
338+ def process (self , output ):
339+ changes = []
340+ for line in output :
341+ line = line .rstrip ("\n " )
342+ if not line or " " not in line :
343+ continue
344+ change , _ , path = line .partition (" " )
345+ changes .append ({"change" : change , "path" : path })
346+ return changes
347+
348+
349+ class DockerContainerProcesses (DockerFactBase ):
350+ """
351+ Returns processes running inside a single container (``docker top``) as a list of
352+ column dicts. Column names follow the ``ps`` output header.
353+ """
354+
355+ @override
356+ def command (self , container_id ) -> str :
357+ return "docker container top {0} 2>&- || true" .format (container_id )
358+
359+ @override
360+ def process (self , output ):
361+ lines = [line for line in (line .rstrip ("\n " ) for line in output ) if line ]
362+ if not lines :
363+ return []
364+ header = lines [0 ].split ()
365+ processes = []
366+ for line in lines [1 :]:
367+ fields = line .split (None , len (header ) - 1 )
368+ if len (fields ) < len (header ):
369+ fields += ["" ] * (len (header ) - len (fields ))
370+ processes .append (dict (zip (header , fields )))
371+ return processes
372+
373+
374+ class DockerContainerStats (_DockerJsonLinesFactBase ):
375+ """
376+ Returns resource-usage stats for all running containers (``docker stats``) as a list
377+ of dicts (one entry per container).
378+ """
379+
380+ @override
381+ def command (self ) -> str :
382+ return "docker stats --no-stream --format '{{json .}}'"
383+
384+
385+ class DockerImageHistory (_DockerJsonLinesFactBase ):
386+ """
387+ Returns the layer history of a single image (``docker history``) as a list of dicts.
388+ """
389+
390+ @override
391+ def command (self , image_id ) -> str :
392+ return (
393+ "docker image history --no-trunc --format '{{{{json .}}}}' {0} 2>&- || true"
394+ ).format (image_id )
0 commit comments