container-manager-mcp 0.0.4__py3-none-any.whl → 1.2.0__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.
Files changed (29) hide show
  1. container_manager_mcp/__init__.py +23 -15
  2. container_manager_mcp/__main__.py +6 -0
  3. container_manager_mcp/container_manager.py +1145 -309
  4. container_manager_mcp/container_manager_a2a.py +339 -0
  5. container_manager_mcp/container_manager_mcp.py +2061 -1110
  6. container_manager_mcp/mcp_config.json +7 -0
  7. container_manager_mcp/skills/container-manager-compose/SKILL.md +25 -0
  8. container_manager_mcp/skills/container-manager-containers/SKILL.md +28 -0
  9. container_manager_mcp/skills/container-manager-containers/troubleshoot.md +5 -0
  10. container_manager_mcp/skills/container-manager-images/SKILL.md +25 -0
  11. container_manager_mcp/skills/container-manager-info/SKILL.md +23 -0
  12. container_manager_mcp/skills/container-manager-logs/SKILL.md +22 -0
  13. container_manager_mcp/skills/container-manager-networks/SKILL.md +22 -0
  14. container_manager_mcp/skills/container-manager-swarm/SKILL.md +28 -0
  15. container_manager_mcp/skills/container-manager-swarm/orchestrate.md +4 -0
  16. container_manager_mcp/skills/container-manager-system/SKILL.md +19 -0
  17. container_manager_mcp/skills/container-manager-volumes/SKILL.md +23 -0
  18. container_manager_mcp/utils.py +31 -0
  19. container_manager_mcp-1.2.0.dist-info/METADATA +371 -0
  20. container_manager_mcp-1.2.0.dist-info/RECORD +26 -0
  21. container_manager_mcp-1.2.0.dist-info/entry_points.txt +4 -0
  22. {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/top_level.txt +1 -0
  23. scripts/validate_a2a_agent.py +150 -0
  24. scripts/validate_agent.py +67 -0
  25. container_manager_mcp-0.0.4.dist-info/METADATA +0 -243
  26. container_manager_mcp-0.0.4.dist-info/RECORD +0 -9
  27. container_manager_mcp-0.0.4.dist-info/entry_points.txt +0 -3
  28. {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/WHEEL +0 -0
  29. {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,13 @@ import logging
6
6
  import os
7
7
  from abc import ABC, abstractmethod
8
8
  from typing import List, Dict, Optional, Any
9
- import getopt
9
+ import argparse
10
10
  import json
11
+ import shutil
11
12
  import subprocess
13
+ from datetime import datetime
14
+ import platform
15
+ import traceback
12
16
 
13
17
  try:
14
18
  import docker
@@ -26,6 +30,7 @@ except ImportError:
26
30
 
27
31
 
28
32
  class ContainerManagerBase(ABC):
33
+
29
34
  def __init__(self, silent: bool = False, log_file: str = None):
30
35
  self.silent = silent
31
36
  self.setup_logging(log_file)
@@ -36,7 +41,7 @@ class ContainerManagerBase(ABC):
36
41
  log_file = os.path.join(script_dir, "container_manager.log")
37
42
  logging.basicConfig(
38
43
  filename=log_file,
39
- level=logging.INFO,
44
+ level=logging.DEBUG, # Changed to DEBUG for more detailed logging
40
45
  format="%(asctime)s - %(levelname)s - %(message)s",
41
46
  )
42
47
  self.logger = logging.getLogger(__name__)
@@ -53,7 +58,55 @@ class ContainerManagerBase(ABC):
53
58
  if result:
54
59
  self.logger.info(f"Result: {result}")
55
60
  if error:
56
- self.logger.error(f"Error: {str(error)}")
61
+ self.logger.error(
62
+ f"Error in {action}: {type(error).__name__}: {str(error)}"
63
+ )
64
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
65
+
66
+ def _format_size(self, size_bytes: int) -> str:
67
+ """Helper to format bytes to human-readable (e.g., 1.23GB)."""
68
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
69
+ if size_bytes < 1024.0:
70
+ return (
71
+ f"{size_bytes:.2f}{unit}" if unit != "B" else f"{size_bytes}{unit}"
72
+ )
73
+ size_bytes /= 1024.0
74
+ return f"{size_bytes:.2f}PB"
75
+
76
+ def _parse_timestamp(self, timestamp: Any) -> str:
77
+ """Parse timestamp (integer, float, or string) to ISO 8601 string."""
78
+ if not timestamp:
79
+ return "unknown"
80
+
81
+ # Handle numeric timestamps (Unix timestamps in seconds)
82
+ if isinstance(timestamp, (int, float)):
83
+ try:
84
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
85
+ except (ValueError, OSError):
86
+ return "unknown"
87
+
88
+ # Handle string timestamps
89
+ if isinstance(timestamp, str):
90
+ # Common ISO 8601-like formats to try
91
+ formats = [
92
+ "%Y-%m-%dT%H:%M:%S", # 2023-10-05T14:30:00
93
+ "%Y-%m-%d %H:%M:%S", # 2023-10-05 14:30:00
94
+ "%Y-%m-%dT%H:%M:%S%z", # 2023-10-05T14:30:00+0000
95
+ "%Y-%m-%d %H:%M:%S%z", # 2023-10-05 14:30:00+0000
96
+ "%Y-%m-%dT%H:%M:%S.%f", # 2023-10-05T14:30:00.123456
97
+ "%Y-%m-%d %H:%M:%S.%f", # 2023-10-05 14:30:00.123456
98
+ "%Y-%m-%dT%H:%M:%S.%f%z", # 2023-10-05T14:30:00.123456+0000
99
+ "%Y-%m-%d", # 2023-10-05
100
+ ]
101
+ for fmt in formats:
102
+ try:
103
+ parsed = datetime.strptime(timestamp, fmt)
104
+ return parsed.strftime("%Y-%m-%dT%H:%M:%S")
105
+ except ValueError:
106
+ continue
107
+ return "unknown"
108
+
109
+ return "unknown"
57
110
 
58
111
  @abstractmethod
59
112
  def get_version(self) -> Dict:
@@ -77,6 +130,10 @@ class ContainerManagerBase(ABC):
77
130
  def remove_image(self, image: str, force: bool = False) -> Dict:
78
131
  pass
79
132
 
133
+ @abstractmethod
134
+ def prune_images(self, force: bool = False, all: bool = False) -> Dict:
135
+ pass
136
+
80
137
  @abstractmethod
81
138
  def list_containers(self, all: bool = False) -> List[Dict]:
82
139
  pass
@@ -102,6 +159,10 @@ class ContainerManagerBase(ABC):
102
159
  def remove_container(self, container_id: str, force: bool = False) -> Dict:
103
160
  pass
104
161
 
162
+ @abstractmethod
163
+ def prune_containers(self) -> Dict:
164
+ pass
165
+
105
166
  @abstractmethod
106
167
  def get_container_logs(self, container_id: str, tail: str = "all") -> str:
107
168
  pass
@@ -124,6 +185,10 @@ class ContainerManagerBase(ABC):
124
185
  def remove_volume(self, name: str, force: bool = False) -> Dict:
125
186
  pass
126
187
 
188
+ @abstractmethod
189
+ def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
190
+ pass
191
+
127
192
  @abstractmethod
128
193
  def list_networks(self) -> List[Dict]:
129
194
  pass
@@ -136,7 +201,14 @@ class ContainerManagerBase(ABC):
136
201
  def remove_network(self, network_id: str) -> Dict:
137
202
  pass
138
203
 
139
- # Compose methods
204
+ @abstractmethod
205
+ def prune_networks(self) -> Dict:
206
+ pass
207
+
208
+ @abstractmethod
209
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
210
+ pass
211
+
140
212
  @abstractmethod
141
213
  def compose_up(
142
214
  self, compose_file: str, detach: bool = True, build: bool = False
@@ -155,19 +227,23 @@ class ContainerManagerBase(ABC):
155
227
  def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
156
228
  pass
157
229
 
158
- # Swarm methods (to be implemented only in DockerManager)
230
+ @abstractmethod
159
231
  def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
160
- raise NotImplementedError("Swarm not supported")
232
+ pass
161
233
 
234
+ @abstractmethod
162
235
  def leave_swarm(self, force: bool = False) -> Dict:
163
- raise NotImplementedError("Swarm not supported")
236
+ pass
164
237
 
238
+ @abstractmethod
165
239
  def list_nodes(self) -> List[Dict]:
166
- raise NotImplementedError("Swarm not supported")
240
+ pass
167
241
 
242
+ @abstractmethod
168
243
  def list_services(self) -> List[Dict]:
169
- raise NotImplementedError("Swarm not supported")
244
+ pass
170
245
 
246
+ @abstractmethod
171
247
  def create_service(
172
248
  self,
173
249
  name: str,
@@ -176,10 +252,11 @@ class ContainerManagerBase(ABC):
176
252
  ports: Optional[Dict[str, str]] = None,
177
253
  mounts: Optional[List[str]] = None,
178
254
  ) -> Dict:
179
- raise NotImplementedError("Swarm not supported")
255
+ pass
180
256
 
257
+ @abstractmethod
181
258
  def remove_service(self, service_id: str) -> Dict:
182
- raise NotImplementedError("Swarm not supported")
259
+ pass
183
260
 
184
261
 
185
262
  class DockerManager(ContainerManagerBase):
@@ -193,10 +270,53 @@ class DockerManager(ContainerManagerBase):
193
270
  self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
194
271
  raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
195
272
 
273
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
274
+ params = {"force": force, "all": all}
275
+ try:
276
+ filters = {"until": None} if all else {}
277
+ result = self.client.system.prune(filters=filters, volumes=all)
278
+ if result is None:
279
+ result = {
280
+ "SpaceReclaimed": 0,
281
+ "ImagesDeleted": [],
282
+ "ContainersDeleted": [],
283
+ "VolumesDeleted": [],
284
+ "NetworksDeleted": [],
285
+ }
286
+ self.logger.debug(f"Raw prune_system result: {result}")
287
+ pruned = {
288
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
289
+ "images_removed": (
290
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
291
+ ),
292
+ "containers_removed": (
293
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
294
+ ),
295
+ "volumes_removed": (
296
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
297
+ ),
298
+ "networks_removed": (
299
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
300
+ ),
301
+ }
302
+ self.log_action("prune_system", params, pruned)
303
+ return pruned
304
+ except Exception as e:
305
+ self.log_action("prune_system", params, error=e)
306
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
307
+
308
+ # Other DockerManager methods remain unchanged (omitted for brevity)
196
309
  def get_version(self) -> Dict:
197
310
  params = {}
198
311
  try:
199
- result = self.client.version()
312
+ version = self.client.version()
313
+ result = {
314
+ "version": version.get("Version", "unknown"),
315
+ "api_version": version.get("ApiVersion", "unknown"),
316
+ "os": version.get("Os", "unknown"),
317
+ "arch": version.get("Arch", "unknown"),
318
+ "build_time": version.get("BuildTime", "unknown"),
319
+ }
200
320
  self.log_action("get_version", params, result)
201
321
  return result
202
322
  except Exception as e:
@@ -206,7 +326,16 @@ class DockerManager(ContainerManagerBase):
206
326
  def get_info(self) -> Dict:
207
327
  params = {}
208
328
  try:
209
- result = self.client.info()
329
+ info = self.client.info()
330
+ result = {
331
+ "containers_total": info.get("Containers", 0),
332
+ "containers_running": info.get("ContainersRunning", 0),
333
+ "images": info.get("Images", 0),
334
+ "driver": info.get("Driver", "unknown"),
335
+ "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
336
+ "memory_total": self._format_size(info.get("MemTotal", 0)),
337
+ "swap_total": self._format_size(info.get("SwapTotal", 0)),
338
+ }
210
339
  self.log_action("get_info", params, result)
211
340
  return result
212
341
  except Exception as e:
@@ -217,7 +346,34 @@ class DockerManager(ContainerManagerBase):
217
346
  params = {}
218
347
  try:
219
348
  images = self.client.images.list()
220
- result = [img.attrs for img in images]
349
+ result = []
350
+ for img in images:
351
+ attrs = img.attrs
352
+ repo_tags = attrs.get("RepoTags", [])
353
+ repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
354
+ repository, tag = (
355
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
356
+ )
357
+
358
+ created = attrs.get("Created", None)
359
+ created_str = self._parse_timestamp(created)
360
+
361
+ size_bytes = attrs.get("Size", 0)
362
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
363
+
364
+ simplified = {
365
+ "repository": repository,
366
+ "tag": tag,
367
+ "id": (
368
+ attrs.get("Id", "unknown")[7:19]
369
+ if attrs.get("Id")
370
+ else "unknown"
371
+ ),
372
+ "created": created_str,
373
+ "size": size_str,
374
+ }
375
+ result.append(simplified)
376
+
221
377
  self.log_action("list_images", params, result)
222
378
  return result
223
379
  except Exception as e:
@@ -230,7 +386,25 @@ class DockerManager(ContainerManagerBase):
230
386
  params = {"image": image, "tag": tag, "platform": platform}
231
387
  try:
232
388
  img = self.client.images.pull(f"{image}:{tag}", platform=platform)
233
- result = img.attrs
389
+ attrs = img.attrs
390
+ repo_tags = attrs.get("RepoTags", [])
391
+ repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
392
+ repository, tag = (
393
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
394
+ )
395
+ created = attrs.get("Created", None)
396
+ created_str = self._parse_timestamp(created)
397
+ size_bytes = attrs.get("Size", 0)
398
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
399
+ result = {
400
+ "repository": repository,
401
+ "tag": tag,
402
+ "id": (
403
+ attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
404
+ ),
405
+ "created": created_str,
406
+ "size": size_str,
407
+ }
234
408
  self.log_action("pull_image", params, result)
235
409
  return result
236
410
  except Exception as e:
@@ -248,11 +422,74 @@ class DockerManager(ContainerManagerBase):
248
422
  self.log_action("remove_image", params, error=e)
249
423
  raise RuntimeError(f"Failed to remove image: {str(e)}")
250
424
 
425
+ def prune_images(self, force: bool = False, all: bool = False) -> Dict:
426
+ params = {"force": force, "all": all}
427
+ try:
428
+ if all:
429
+ # Manually remove all unused images
430
+ images = self.client.images.list(all=True)
431
+ removed = []
432
+ for img in images:
433
+ try:
434
+ for tag in img.attrs.get("RepoTags", []):
435
+ self.client.images.remove(tag, force=force)
436
+ removed.append(img.attrs["Id"][7:19])
437
+ except Exception as e:
438
+ self.logger.info(
439
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
440
+ )
441
+ continue
442
+ result = {
443
+ "images_removed": removed,
444
+ "space_reclaimed": "N/A (all images)",
445
+ }
446
+ else:
447
+ filters = {"dangling": True} if not all else {}
448
+ result = self.client.images.prune(filters=filters)
449
+ if result is None:
450
+ result = {"SpaceReclaimed": 0, "ImagesDeleted": []}
451
+ self.logger.debug(f"Raw prune_images result: {result}")
452
+ pruned = {
453
+ "space_reclaimed": self._format_size(
454
+ result.get("SpaceReclaimed", 0)
455
+ ),
456
+ "images_removed": (
457
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
458
+ ),
459
+ }
460
+ result = pruned
461
+ self.log_action("prune_images", params, result)
462
+ return result
463
+ except Exception as e:
464
+ self.log_action("prune_images", params, error=e)
465
+ raise RuntimeError(f"Failed to prune images: {str(e)}")
466
+
251
467
  def list_containers(self, all: bool = False) -> List[Dict]:
252
468
  params = {"all": all}
253
469
  try:
254
470
  containers = self.client.containers.list(all=all)
255
- result = [c.attrs for c in containers]
471
+ result = []
472
+ for c in containers:
473
+ attrs = c.attrs
474
+ ports = attrs.get("NetworkSettings", {}).get("Ports", {})
475
+ port_mappings = []
476
+ for container_port, host_ports in ports.items():
477
+ if host_ports:
478
+ for hp in host_ports:
479
+ port_mappings.append(
480
+ f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
481
+ )
482
+ created = attrs.get("Created", None)
483
+ created_str = self._parse_timestamp(created)
484
+ simplified = {
485
+ "id": attrs.get("Id", "unknown")[7:19],
486
+ "image": attrs.get("Config", {}).get("Image", "unknown"),
487
+ "name": attrs.get("Name", "unknown").lstrip("/"),
488
+ "status": attrs.get("State", {}).get("Status", "unknown"),
489
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
490
+ "created": created_str,
491
+ }
492
+ result.append(simplified)
256
493
  self.log_action("list_containers", params, result)
257
494
  return result
258
495
  except Exception as e:
@@ -280,7 +517,7 @@ class DockerManager(ContainerManagerBase):
280
517
  }
281
518
  try:
282
519
  container = self.client.containers.run(
283
- image,
520
+ image=image,
284
521
  command=command,
285
522
  name=name,
286
523
  detach=detach,
@@ -288,9 +525,32 @@ class DockerManager(ContainerManagerBase):
288
525
  volumes=volumes,
289
526
  environment=environment,
290
527
  )
291
- result = (
292
- container.attrs if detach else {"output": container.decode("utf-8")}
293
- )
528
+ if not detach:
529
+ result = {"output": container.decode("utf-8") if container else ""}
530
+ self.log_action("run_container", params, result)
531
+ return result
532
+ attrs = container.attrs
533
+ port_mappings = []
534
+ if ports: # Check if ports is not None
535
+ network_settings = attrs.get("NetworkSettings", {})
536
+ container_ports = network_settings.get("Ports", {})
537
+ if container_ports: # Check if Ports dictionary is not empty
538
+ for container_port, host_ports in container_ports.items():
539
+ if host_ports: # Check if host_ports is not None or empty
540
+ for hp in host_ports:
541
+ port_mappings.append(
542
+ f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
543
+ )
544
+ created = attrs.get("Created", None)
545
+ created_str = self._parse_timestamp(created)
546
+ result = {
547
+ "id": attrs.get("Id", "unknown")[7:19],
548
+ "image": attrs.get("Config", {}).get("Image", image),
549
+ "name": attrs.get("Name", name or "unknown").lstrip("/"),
550
+ "status": attrs.get("State", {}).get("Status", "unknown"),
551
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
552
+ "created": created_str,
553
+ }
294
554
  self.log_action("run_container", params, result)
295
555
  return result
296
556
  except Exception as e:
@@ -321,12 +581,42 @@ class DockerManager(ContainerManagerBase):
321
581
  self.log_action("remove_container", params, error=e)
322
582
  raise RuntimeError(f"Failed to remove container: {str(e)}")
323
583
 
584
+ def prune_containers(self) -> Dict:
585
+ params = {}
586
+ try:
587
+ result = self.client.containers.prune()
588
+ self.logger.debug(f"Raw prune_containers result: {result}")
589
+ if result is None:
590
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
591
+ pruned = {
592
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
593
+ "containers_removed": (
594
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
595
+ ),
596
+ }
597
+ self.log_action("prune_containers", params, pruned)
598
+ return pruned
599
+ except TypeError as e:
600
+ self.logger.error(f"TypeError in prune_containers: {str(e)}")
601
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
602
+ self.log_action("prune_containers", params, error=e)
603
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
604
+ except Exception as e:
605
+ self.logger.error(
606
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
607
+ )
608
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
609
+ self.log_action("prune_containers", params, error=e)
610
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
611
+
324
612
  def get_container_logs(self, container_id: str, tail: str = "all") -> str:
325
613
  params = {"container_id": container_id, "tail": tail}
326
614
  try:
327
615
  container = self.client.containers.get(container_id)
328
616
  logs = container.logs(tail=tail).decode("utf-8")
329
- self.log_action("get_container_logs", params, logs)
617
+ self.log_action(
618
+ "get_container_logs", params, logs[:1000]
619
+ ) # Truncate for logging
330
620
  return logs
331
621
  except Exception as e:
332
622
  self.log_action("get_container_logs", params, error=e)
@@ -341,7 +631,8 @@ class DockerManager(ContainerManagerBase):
341
631
  exit_code, output = container.exec_run(command, detach=detach)
342
632
  result = {
343
633
  "exit_code": exit_code,
344
- "output": output.decode("utf-8") if output else None,
634
+ "output": output.decode("utf-8") if output and not detach else None,
635
+ "command": command,
345
636
  }
346
637
  self.log_action("exec_in_container", params, result)
347
638
  return result
@@ -353,7 +644,17 @@ class DockerManager(ContainerManagerBase):
353
644
  params = {}
354
645
  try:
355
646
  volumes = self.client.volumes.list()
356
- result = {"volumes": [v.attrs for v in volumes]}
647
+ result = {
648
+ "volumes": [
649
+ {
650
+ "name": v.attrs.get("Name", "unknown"),
651
+ "driver": v.attrs.get("Driver", "unknown"),
652
+ "mountpoint": v.attrs.get("Mountpoint", "unknown"),
653
+ "created": v.attrs.get("CreatedAt", "unknown"),
654
+ }
655
+ for v in volumes
656
+ ]
657
+ }
357
658
  self.log_action("list_volumes", params, result)
358
659
  return result
359
660
  except Exception as e:
@@ -364,7 +665,13 @@ class DockerManager(ContainerManagerBase):
364
665
  params = {"name": name}
365
666
  try:
366
667
  volume = self.client.volumes.create(name=name)
367
- result = volume.attrs
668
+ attrs = volume.attrs
669
+ result = {
670
+ "name": attrs.get("Name", name),
671
+ "driver": attrs.get("Driver", "unknown"),
672
+ "mountpoint": attrs.get("Mountpoint", "unknown"),
673
+ "created": attrs.get("CreatedAt", "unknown"),
674
+ }
368
675
  self.log_action("create_volume", params, result)
369
676
  return result
370
677
  except Exception as e:
@@ -383,11 +690,64 @@ class DockerManager(ContainerManagerBase):
383
690
  self.log_action("remove_volume", params, error=e)
384
691
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
385
692
 
693
+ def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
694
+ params = {"force": force, "all": all}
695
+ try:
696
+ if all:
697
+ volumes = self.client.volumes.list(all=True)
698
+ removed = []
699
+ for v in volumes:
700
+ try:
701
+ v.remove(force=force)
702
+ removed.append(v.attrs["Name"])
703
+ except Exception as e:
704
+ self.logger.info(
705
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
706
+ )
707
+ continue
708
+ result = {
709
+ "volumes_removed": removed,
710
+ "space_reclaimed": "N/A (all volumes)",
711
+ }
712
+ else:
713
+ result = self.client.volumes.prune()
714
+ if result is None:
715
+ result = {"SpaceReclaimed": 0, "VolumesDeleted": []}
716
+ self.logger.debug(f"Raw prune_volumes result: {result}")
717
+ pruned = {
718
+ "space_reclaimed": self._format_size(
719
+ result.get("SpaceReclaimed", 0)
720
+ ),
721
+ "volumes_removed": (
722
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
723
+ ),
724
+ }
725
+ result = pruned
726
+ self.log_action("prune_volumes", params, result)
727
+ return result
728
+ except Exception as e:
729
+ self.log_action("prune_volumes", params, error=e)
730
+ raise RuntimeError(f"Failed to prune volumes: {str(e)}")
731
+
386
732
  def list_networks(self) -> List[Dict]:
387
733
  params = {}
388
734
  try:
389
735
  networks = self.client.networks.list()
390
- result = [net.attrs for net in networks]
736
+ result = []
737
+ for net in networks:
738
+ attrs = net.attrs
739
+ containers = len(attrs.get("Containers", {}))
740
+ created = attrs.get("Created", None)
741
+ created_str = self._parse_timestamp(created)
742
+ simplified = {
743
+ "id": attrs.get("Id", "unknown")[7:19],
744
+ "name": attrs.get("Name", "unknown"),
745
+ "driver": attrs.get("Driver", "unknown"),
746
+ "scope": attrs.get("Scope", "unknown"),
747
+ "containers": containers,
748
+ "created": created_str,
749
+ }
750
+ result.append(simplified)
391
751
  self.log_action("list_networks", params, result)
392
752
  return result
393
753
  except Exception as e:
@@ -398,7 +758,16 @@ class DockerManager(ContainerManagerBase):
398
758
  params = {"name": name, "driver": driver}
399
759
  try:
400
760
  network = self.client.networks.create(name, driver=driver)
401
- result = network.attrs
761
+ attrs = network.attrs
762
+ created = attrs.get("Created", None)
763
+ created_str = self._parse_timestamp(created)
764
+ result = {
765
+ "id": attrs.get("Id", "unknown")[7:19],
766
+ "name": attrs.get("Name", name),
767
+ "driver": attrs.get("Driver", driver),
768
+ "scope": attrs.get("Scope", "unknown"),
769
+ "created": created_str,
770
+ }
402
771
  self.log_action("create_network", params, result)
403
772
  return result
404
773
  except Exception as e:
@@ -417,6 +786,25 @@ class DockerManager(ContainerManagerBase):
417
786
  self.log_action("remove_network", params, error=e)
418
787
  raise RuntimeError(f"Failed to remove network: {str(e)}")
419
788
 
789
+ def prune_networks(self) -> Dict:
790
+ params = {}
791
+ try:
792
+ result = self.client.networks.prune()
793
+ if result is None:
794
+ result = {"SpaceReclaimed": 0, "NetworksDeleted": []}
795
+ self.logger.debug(f"Raw prune_networks result: {result}")
796
+ pruned = {
797
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
798
+ "networks_removed": (
799
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
800
+ ),
801
+ }
802
+ self.log_action("prune_networks", params, pruned)
803
+ return pruned
804
+ except Exception as e:
805
+ self.log_action("prune_networks", params, error=e)
806
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
807
+
420
808
  def compose_up(
421
809
  self, compose_file: str, detach: bool = True, build: bool = False
422
810
  ) -> str:
@@ -503,7 +891,23 @@ class DockerManager(ContainerManagerBase):
503
891
  params = {}
504
892
  try:
505
893
  nodes = self.client.nodes.list()
506
- result = [node.attrs for node in nodes]
894
+ result = []
895
+ for node in nodes:
896
+ attrs = node.attrs
897
+ spec = attrs.get("Spec", {})
898
+ status = attrs.get("Status", {})
899
+ created = attrs.get("CreatedAt", "unknown")
900
+ updated = attrs.get("UpdatedAt", "unknown")
901
+ simplified = {
902
+ "id": attrs.get("ID", "unknown")[7:19],
903
+ "hostname": spec.get("Name", "unknown"),
904
+ "role": spec.get("Role", "unknown"),
905
+ "status": status.get("State", "unknown"),
906
+ "availability": spec.get("Availability", "unknown"),
907
+ "created": created,
908
+ "updated": updated,
909
+ }
910
+ result.append(simplified)
507
911
  self.log_action("list_nodes", params, result)
508
912
  return result
509
913
  except Exception as e:
@@ -514,7 +918,33 @@ class DockerManager(ContainerManagerBase):
514
918
  params = {}
515
919
  try:
516
920
  services = self.client.services.list()
517
- result = [service.attrs for service in services]
921
+ result = []
922
+ for service in services:
923
+ attrs = service.attrs
924
+ spec = attrs.get("Spec", {})
925
+ endpoint = attrs.get("Endpoint", {})
926
+ ports = endpoint.get("Ports", [])
927
+ port_mappings = [
928
+ f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
929
+ for p in ports
930
+ if p.get("PublishedPort")
931
+ ]
932
+ created = attrs.get("CreatedAt", "unknown")
933
+ updated = attrs.get("UpdatedAt", "unknown")
934
+ simplified = {
935
+ "id": attrs.get("ID", "unknown")[7:19],
936
+ "name": spec.get("Name", "unknown"),
937
+ "image": spec.get("TaskTemplate", {})
938
+ .get("ContainerSpec", {})
939
+ .get("Image", "unknown"),
940
+ "replicas": spec.get("Mode", {})
941
+ .get("Replicated", {})
942
+ .get("Replicas", 0),
943
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
944
+ "created": created,
945
+ "updated": updated,
946
+ }
947
+ result.append(simplified)
518
948
  self.log_action("list_services", params, result)
519
949
  return result
520
950
  except Exception as e:
@@ -538,15 +968,46 @@ class DockerManager(ContainerManagerBase):
538
968
  }
539
969
  try:
540
970
  mode = {"mode": "replicated", "replicas": replicas}
541
- target_ports = [docker.types.EndpointSpec(ports=ports)] if ports else None
971
+ endpoint_spec = None
972
+ if ports:
973
+ port_list = [
974
+ {
975
+ "Protocol": "tcp",
976
+ "PublishedPort": int(host_port),
977
+ "TargetPort": int(container_port.split("/")[0]),
978
+ }
979
+ for container_port, host_port in ports.items()
980
+ ]
981
+ endpoint_spec = docker.types.EndpointSpec(ports=port_list)
542
982
  service = self.client.services.create(
543
983
  image,
544
984
  name=name,
545
985
  mode=mode,
546
986
  mounts=mounts,
547
- endpoint_spec=target_ports[0] if target_ports else None,
987
+ endpoint_spec=endpoint_spec,
548
988
  )
549
- result = service.attrs
989
+ attrs = service.attrs
990
+ spec = attrs.get("Spec", {})
991
+ endpoint = attrs.get("Endpoint", {})
992
+ ports = endpoint.get("Ports", [])
993
+ port_mappings = [
994
+ f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
995
+ for p in ports
996
+ if p.get("PublishedPort")
997
+ ]
998
+ created = attrs.get("CreatedAt", "unknown")
999
+ result = {
1000
+ "id": attrs.get("ID", "unknown")[7:19],
1001
+ "name": spec.get("Name", name),
1002
+ "image": spec.get("TaskTemplate", {})
1003
+ .get("ContainerSpec", {})
1004
+ .get("Image", image),
1005
+ "replicas": spec.get("Mode", {})
1006
+ .get("Replicated", {})
1007
+ .get("Replicas", replicas),
1008
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
1009
+ "created": created,
1010
+ }
550
1011
  self.log_action("create_service", params, result)
551
1012
  return result
552
1013
  except Exception as e:
@@ -567,20 +1028,268 @@ class DockerManager(ContainerManagerBase):
567
1028
 
568
1029
 
569
1030
  class PodmanManager(ContainerManagerBase):
570
- def __init__(self, silent: bool = False, log_file: str = None):
1031
+ def __init__(self, silent: bool = False, log_file: Optional[str] = None):
571
1032
  super().__init__(silent, log_file)
572
1033
  if PodmanClient is None:
573
1034
  raise ImportError("Please install podman-py: pip install podman")
1035
+ base_url = self._autodetect_podman_url()
1036
+ if base_url is None:
1037
+ self.logger.error(
1038
+ "No valid Podman socket found after trying all known locations"
1039
+ )
1040
+ raise RuntimeError("Failed to connect to Podman: No valid socket found")
1041
+ try:
1042
+ self.client = PodmanClient(base_url=base_url)
1043
+ self.logger.info(f"Connected to Podman with base_url: {base_url}")
1044
+ except PodmanError as e:
1045
+ self.logger.error(
1046
+ f"Failed to connect to Podman daemon with {base_url}: {str(e)}"
1047
+ )
1048
+ raise RuntimeError(f"Failed to connect to Podman with {base_url}: {str(e)}")
1049
+
1050
+ def _is_wsl(self) -> bool:
1051
+ """Check if running inside WSL2."""
1052
+ try:
1053
+ with open("/proc/version", "r") as f:
1054
+ return "WSL" in f.read()
1055
+ except FileNotFoundError:
1056
+ return "WSL_DISTRO_NAME" in os.environ
1057
+
1058
+ def _is_podman_machine_running(self) -> bool:
1059
+ """Check if Podman machine is running (for Windows/WSL2)."""
1060
+ try:
1061
+ result = subprocess.run(
1062
+ ["podman", "machine", "list", "--format", "{{.Running}}"],
1063
+ capture_output=True,
1064
+ text=True,
1065
+ check=False,
1066
+ )
1067
+ return "true" in result.stdout.lower()
1068
+ except (subprocess.SubprocessError, FileNotFoundError):
1069
+ return False
1070
+
1071
+ def _try_connect(self, base_url: str) -> Optional[PodmanClient]:
1072
+ """Attempt to connect to Podman with the given base_url."""
574
1073
  try:
575
- self.client = PodmanClient()
1074
+ client = PodmanClient(base_url=base_url)
1075
+ client.version()
1076
+ return client
576
1077
  except PodmanError as e:
577
- self.logger.error(f"Failed to connect to Podman daemon: {str(e)}")
578
- raise RuntimeError(f"Failed to connect to Podman: {str(e)}")
1078
+ self.logger.debug(f"Connection failed for {base_url}: {str(e)}")
1079
+ return None
1080
+
1081
+ def _autodetect_podman_url(self) -> Optional[str]:
1082
+ """Autodetect the appropriate Podman socket URL based on platform."""
1083
+ base_url = os.environ.get("CONTAINER_MANAGER_PODMAN_BASE_URL")
1084
+ if base_url:
1085
+ self.logger.info(
1086
+ f"Using CONTAINER_MANAGER_PODMAN_BASE_URL from environment: {base_url}"
1087
+ )
1088
+ return base_url
1089
+ system = platform.system()
1090
+ is_wsl = self._is_wsl()
1091
+ socket_candidates = []
1092
+ if system == "Windows" and not is_wsl:
1093
+ if not self._is_podman_machine_running():
1094
+ raise RuntimeError("Podman Machine is not running on Windows system")
1095
+ socket_candidates.extend(
1096
+ [
1097
+ "tcp://127.0.0.1:8080",
1098
+ "unix:///run/podman/podman.sock",
1099
+ "npipe:////./pipe/docker_engine",
1100
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
1101
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
1102
+ ]
1103
+ )
1104
+ elif system == "Linux" or is_wsl:
1105
+ uid = os.getuid()
1106
+ socket_candidates.extend(
1107
+ [
1108
+ f"unix:///run/user/{uid}/podman/podman.sock",
1109
+ "unix:///run/podman/podman.sock",
1110
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
1111
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
1112
+ ]
1113
+ )
1114
+ for url in socket_candidates:
1115
+ client = self._try_connect(url)
1116
+ if client:
1117
+ return url
1118
+ return None
1119
+
1120
+ def prune_images(self, force: bool = False, all: bool = False) -> Dict:
1121
+ params = {"force": force, "all": all}
1122
+ try:
1123
+ if all:
1124
+ # Manually remove all unused images
1125
+ images = self.client.images.list(all=True)
1126
+ removed = []
1127
+ for img in images:
1128
+ try:
1129
+ for tag in img.attrs.get("Names", []):
1130
+ self.client.images.remove(tag, force=force)
1131
+ removed.append(img.attrs["Id"][7:19])
1132
+ except Exception as e:
1133
+ self.logger.info(
1134
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
1135
+ )
1136
+ continue
1137
+ result = {
1138
+ "images_removed": removed,
1139
+ "space_reclaimed": "N/A (all images)",
1140
+ }
1141
+ else:
1142
+ filters = {"dangling": True} if not all else {}
1143
+ result = self.client.images.prune(filters=filters)
1144
+ if result is None:
1145
+ result = {"SpaceReclaimed": 0, "ImagesRemoved": []}
1146
+ self.logger.debug(f"Raw prune_images result: {result}")
1147
+ pruned = {
1148
+ "space_reclaimed": self._format_size(
1149
+ result.get("SpaceReclaimed", 0)
1150
+ ),
1151
+ "images_removed": (
1152
+ [img["Id"][7:19] for img in result.get("ImagesRemoved", [])]
1153
+ or [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
1154
+ ),
1155
+ }
1156
+ result = pruned
1157
+ self.log_action("prune_images", params, result)
1158
+ return result
1159
+ except Exception as e:
1160
+ self.log_action("prune_images", params, error=e)
1161
+ raise RuntimeError(f"Failed to prune images: {str(e)}")
1162
+
1163
+ def prune_containers(self) -> Dict:
1164
+ params = {}
1165
+ try:
1166
+ result = self.client.containers.prune()
1167
+ self.logger.debug(f"Raw prune_containers result: {result}")
1168
+ if result is None:
1169
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
1170
+ pruned = {
1171
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1172
+ "containers_removed": (
1173
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
1174
+ or [c["Id"][7:19] for c in result.get("ContainersRemoved", [])]
1175
+ ),
1176
+ }
1177
+ self.log_action("prune_containers", params, pruned)
1178
+ return pruned
1179
+ except PodmanError as e:
1180
+ self.logger.error(f"PodmanError in prune_containers: {str(e)}")
1181
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1182
+ self.log_action("prune_containers", params, error=e)
1183
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1184
+ except Exception as e:
1185
+ self.logger.error(
1186
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
1187
+ )
1188
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1189
+ self.log_action("prune_containers", params, error=e)
1190
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1191
+
1192
+ def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
1193
+ params = {"force": force, "all": all}
1194
+ try:
1195
+ if all:
1196
+ volumes = self.client.volumes.list(all=True)
1197
+ removed = []
1198
+ for v in volumes:
1199
+ try:
1200
+ v.remove(force=force)
1201
+ removed.append(v.attrs["Name"])
1202
+ except Exception as e:
1203
+ self.logger.info(
1204
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
1205
+ )
1206
+ continue
1207
+ result = {
1208
+ "volumes_removed": removed,
1209
+ "space_reclaimed": "N/A (all volumes)",
1210
+ }
1211
+ else:
1212
+ result = self.client.volumes.prune()
1213
+ if result is None:
1214
+ result = {"SpaceReclaimed": 0, "VolumesRemoved": []}
1215
+ self.logger.debug(f"Raw prune_volumes result: {result}")
1216
+ pruned = {
1217
+ "space_reclaimed": self._format_size(
1218
+ result.get("SpaceReclaimed", 0)
1219
+ ),
1220
+ "volumes_removed": (
1221
+ [v["Name"] for v in result.get("VolumesRemoved", [])]
1222
+ or [v["Name"] for v in result.get("VolumesDeleted", [])]
1223
+ ),
1224
+ }
1225
+ result = pruned
1226
+ self.log_action("prune_volumes", params, result)
1227
+ return result
1228
+ except Exception as e:
1229
+ self.log_action("prune_volumes", params, error=e)
1230
+ raise RuntimeError(f"Failed to prune volumes: {str(e)}")
1231
+
1232
+ def prune_networks(self) -> Dict:
1233
+ params = {}
1234
+ try:
1235
+ result = self.client.networks.prune()
1236
+ if result is None:
1237
+ result = {"SpaceReclaimed": 0, "NetworksRemoved": []}
1238
+ self.logger.debug(f"Raw prune_networks result: {result}")
1239
+ pruned = {
1240
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1241
+ "networks_removed": (
1242
+ [n["Id"][7:19] for n in result.get("NetworksRemoved", [])]
1243
+ or [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
1244
+ ),
1245
+ }
1246
+ self.log_action("prune_networks", params, pruned)
1247
+ return pruned
1248
+ except Exception as e:
1249
+ self.log_action("prune_networks", params, error=e)
1250
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
1251
+
1252
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
1253
+ params = {"force": force, "all": all}
1254
+ try:
1255
+ cmd = (
1256
+ ["podman", "system", "prune", "--force"]
1257
+ if force
1258
+ else ["podman", "system", "prune"]
1259
+ )
1260
+ if all:
1261
+ cmd.append("--all")
1262
+ if all: # Include volumes if all=True
1263
+ cmd.append("--volumes")
1264
+ result = subprocess.run(cmd, capture_output=True, text=True)
1265
+ if result.returncode != 0:
1266
+ raise RuntimeError(result.stderr)
1267
+ self.logger.debug(f"Raw prune_system result: {result.stdout}")
1268
+ pruned = {
1269
+ "output": result.stdout.strip(),
1270
+ "space_reclaimed": "Check output",
1271
+ "images_removed": [], # Podman CLI doesn't provide detailed breakdown
1272
+ "containers_removed": [],
1273
+ "volumes_removed": [],
1274
+ "networks_removed": [],
1275
+ }
1276
+ self.log_action("prune_system", params, pruned)
1277
+ return pruned
1278
+ except Exception as e:
1279
+ self.log_action("prune_system", params, error=e)
1280
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
579
1281
 
580
1282
  def get_version(self) -> Dict:
581
1283
  params = {}
582
1284
  try:
583
- result = self.client.version()
1285
+ version = self.client.version()
1286
+ result = {
1287
+ "version": version.get("Version", "unknown"),
1288
+ "api_version": version.get("APIVersion", "unknown"),
1289
+ "os": version.get("Os", "unknown"),
1290
+ "arch": version.get("Arch", "unknown"),
1291
+ "build_time": version.get("BuildTime", "unknown"),
1292
+ }
584
1293
  self.log_action("get_version", params, result)
585
1294
  return result
586
1295
  except Exception as e:
@@ -590,7 +1299,17 @@ class PodmanManager(ContainerManagerBase):
590
1299
  def get_info(self) -> Dict:
591
1300
  params = {}
592
1301
  try:
593
- result = self.client.info()
1302
+ info = self.client.info()
1303
+ host = info.get("host", {})
1304
+ result = {
1305
+ "containers_total": info.get("store", {}).get("containers", 0),
1306
+ "containers_running": host.get("runningContainers", 0),
1307
+ "images": info.get("store", {}).get("images", 0),
1308
+ "driver": host.get("graphDriverName", "unknown"),
1309
+ "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1310
+ "memory_total": self._format_size(host.get("memTotal", 0)),
1311
+ "swap_total": self._format_size(host.get("swapTotal", 0)),
1312
+ }
594
1313
  self.log_action("get_info", params, result)
595
1314
  return result
596
1315
  except Exception as e:
@@ -601,7 +1320,30 @@ class PodmanManager(ContainerManagerBase):
601
1320
  params = {}
602
1321
  try:
603
1322
  images = self.client.images.list()
604
- result = [img.attrs for img in images]
1323
+ result = []
1324
+ for img in images:
1325
+ attrs = img.attrs
1326
+ repo_tags = attrs.get("Names", [])
1327
+ repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
1328
+ repository, tag = (
1329
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
1330
+ )
1331
+ created = attrs.get("Created", None)
1332
+ created_str = self._parse_timestamp(created)
1333
+ size_bytes = attrs.get("Size", 0)
1334
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
1335
+ simplified = {
1336
+ "repository": repository,
1337
+ "tag": tag,
1338
+ "id": (
1339
+ attrs.get("Id", "unknown")[7:19]
1340
+ if attrs.get("Id")
1341
+ else "unknown"
1342
+ ),
1343
+ "created": created_str,
1344
+ "size": size_str,
1345
+ }
1346
+ result.append(simplified)
605
1347
  self.log_action("list_images", params, result)
606
1348
  return result
607
1349
  except Exception as e:
@@ -614,7 +1356,25 @@ class PodmanManager(ContainerManagerBase):
614
1356
  params = {"image": image, "tag": tag, "platform": platform}
615
1357
  try:
616
1358
  img = self.client.images.pull(f"{image}:{tag}", platform=platform)
617
- result = img[0].attrs if isinstance(img, list) else img.attrs
1359
+ attrs = img[0].attrs if isinstance(img, list) else img.attrs
1360
+ repo_tags = attrs.get("Names", [])
1361
+ repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
1362
+ repository, tag = (
1363
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
1364
+ )
1365
+ created = attrs.get("Created", None)
1366
+ created_str = self._parse_timestamp(created)
1367
+ size_bytes = attrs.get("Size", 0)
1368
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
1369
+ result = {
1370
+ "repository": repository,
1371
+ "tag": tag,
1372
+ "id": (
1373
+ attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
1374
+ ),
1375
+ "created": created_str,
1376
+ "size": size_str,
1377
+ }
618
1378
  self.log_action("pull_image", params, result)
619
1379
  return result
620
1380
  except Exception as e:
@@ -636,7 +1396,26 @@ class PodmanManager(ContainerManagerBase):
636
1396
  params = {"all": all}
637
1397
  try:
638
1398
  containers = self.client.containers.list(all=all)
639
- result = [c.attrs for c in containers]
1399
+ result = []
1400
+ for c in containers:
1401
+ attrs = c.attrs
1402
+ ports = attrs.get("Ports", [])
1403
+ port_mappings = [
1404
+ f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
1405
+ for p in ports
1406
+ if p.get("host_port")
1407
+ ]
1408
+ created = attrs.get("Created", None)
1409
+ created_str = self._parse_timestamp(created)
1410
+ simplified = {
1411
+ "id": attrs.get("Id", "unknown")[7:19],
1412
+ "image": attrs.get("Image", "unknown"),
1413
+ "name": attrs.get("Names", ["unknown"])[0].lstrip("/"),
1414
+ "status": attrs.get("State", "unknown"),
1415
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
1416
+ "created": created_str,
1417
+ }
1418
+ result.append(simplified)
640
1419
  self.log_action("list_containers", params, result)
641
1420
  return result
642
1421
  except Exception as e:
@@ -672,9 +1451,30 @@ class PodmanManager(ContainerManagerBase):
672
1451
  volumes=volumes,
673
1452
  environment=environment,
674
1453
  )
675
- result = (
676
- container.attrs if detach else {"output": container.decode("utf-8")}
677
- )
1454
+ if not detach:
1455
+ result = {"output": container.decode("utf-8") if container else ""}
1456
+ self.log_action("run_container", params, result)
1457
+ return result
1458
+ attrs = container.attrs
1459
+ port_mappings = []
1460
+ if ports: # Check if ports is not None
1461
+ container_ports = attrs.get("Ports", [])
1462
+ if container_ports: # Check if Ports list is not empty
1463
+ port_mappings = [
1464
+ f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
1465
+ for p in container_ports
1466
+ if p.get("host_port")
1467
+ ]
1468
+ created = attrs.get("Created", None)
1469
+ created_str = self._parse_timestamp(created)
1470
+ result = {
1471
+ "id": attrs.get("Id", "unknown")[7:19],
1472
+ "image": attrs.get("Image", image),
1473
+ "name": attrs.get("Names", [name or "unknown"])[0].lstrip("/"),
1474
+ "status": attrs.get("State", "unknown"),
1475
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
1476
+ "created": created_str,
1477
+ }
678
1478
  self.log_action("run_container", params, result)
679
1479
  return result
680
1480
  except Exception as e:
@@ -710,7 +1510,9 @@ class PodmanManager(ContainerManagerBase):
710
1510
  try:
711
1511
  container = self.client.containers.get(container_id)
712
1512
  logs = container.logs(tail=tail).decode("utf-8")
713
- self.log_action("get_container_logs", params, logs)
1513
+ self.log_action(
1514
+ "get_container_logs", params, logs[:1000]
1515
+ ) # Truncate for logging
714
1516
  return logs
715
1517
  except Exception as e:
716
1518
  self.log_action("get_container_logs", params, error=e)
@@ -725,7 +1527,8 @@ class PodmanManager(ContainerManagerBase):
725
1527
  exit_code, output = container.exec_run(command, detach=detach)
726
1528
  result = {
727
1529
  "exit_code": exit_code,
728
- "output": output.decode("utf-8") if output else None,
1530
+ "output": output.decode("utf-8") if output and not detach else None,
1531
+ "command": command,
729
1532
  }
730
1533
  self.log_action("exec_in_container", params, result)
731
1534
  return result
@@ -737,7 +1540,17 @@ class PodmanManager(ContainerManagerBase):
737
1540
  params = {}
738
1541
  try:
739
1542
  volumes = self.client.volumes.list()
740
- result = {"volumes": [v.attrs for v in volumes]}
1543
+ result = {
1544
+ "volumes": [
1545
+ {
1546
+ "name": v.attrs.get("Name", "unknown"),
1547
+ "driver": v.attrs.get("Driver", "unknown"),
1548
+ "mountpoint": v.attrs.get("Mountpoint", "unknown"),
1549
+ "created": v.attrs.get("CreatedAt", "unknown"),
1550
+ }
1551
+ for v in volumes
1552
+ ]
1553
+ }
741
1554
  self.log_action("list_volumes", params, result)
742
1555
  return result
743
1556
  except Exception as e:
@@ -748,7 +1561,13 @@ class PodmanManager(ContainerManagerBase):
748
1561
  params = {"name": name}
749
1562
  try:
750
1563
  volume = self.client.volumes.create(name=name)
751
- result = volume.attrs
1564
+ attrs = volume.attrs
1565
+ result = {
1566
+ "name": attrs.get("Name", name),
1567
+ "driver": attrs.get("Driver", "unknown"),
1568
+ "mountpoint": attrs.get("Mountpoint", "unknown"),
1569
+ "created": attrs.get("CreatedAt", "unknown"),
1570
+ }
752
1571
  self.log_action("create_volume", params, result)
753
1572
  return result
754
1573
  except Exception as e:
@@ -771,7 +1590,21 @@ class PodmanManager(ContainerManagerBase):
771
1590
  params = {}
772
1591
  try:
773
1592
  networks = self.client.networks.list()
774
- result = [net.attrs for net in networks]
1593
+ result = []
1594
+ for net in networks:
1595
+ attrs = net.attrs
1596
+ containers = len(attrs.get("Containers", {}))
1597
+ created = attrs.get("Created", None)
1598
+ created_str = self._parse_timestamp(created)
1599
+ simplified = {
1600
+ "id": attrs.get("Id", "unknown")[7:19],
1601
+ "name": attrs.get("Name", "unknown"),
1602
+ "driver": attrs.get("Driver", "unknown"),
1603
+ "scope": attrs.get("Scope", "unknown"),
1604
+ "containers": containers,
1605
+ "created": created_str,
1606
+ }
1607
+ result.append(simplified)
775
1608
  self.log_action("list_networks", params, result)
776
1609
  return result
777
1610
  except Exception as e:
@@ -782,7 +1615,16 @@ class PodmanManager(ContainerManagerBase):
782
1615
  params = {"name": name, "driver": driver}
783
1616
  try:
784
1617
  network = self.client.networks.create(name, driver=driver)
785
- result = network.attrs
1618
+ attrs = network.attrs
1619
+ created = attrs.get("Created", None)
1620
+ created_str = self._parse_timestamp(created)
1621
+ result = {
1622
+ "id": attrs.get("Id", "unknown")[7:19],
1623
+ "name": attrs.get("Name", name),
1624
+ "driver": attrs.get("Driver", driver),
1625
+ "scope": attrs.get("Scope", "unknown"),
1626
+ "created": created_str,
1627
+ }
786
1628
  self.log_action("create_network", params, result)
787
1629
  return result
788
1630
  except Exception as e:
@@ -861,11 +1703,51 @@ class PodmanManager(ContainerManagerBase):
861
1703
  self.log_action("compose_logs", params, error=e)
862
1704
  raise RuntimeError(f"Failed to compose logs: {str(e)}")
863
1705
 
1706
+ def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
1707
+ raise NotImplementedError("Swarm not supported in Podman")
1708
+
1709
+ def leave_swarm(self, force: bool = False) -> Dict:
1710
+ raise NotImplementedError("Swarm not supported in Podman")
1711
+
1712
+ def list_nodes(self) -> List[Dict]:
1713
+ raise NotImplementedError("Swarm not supported in Podman")
1714
+
1715
+ def list_services(self) -> List[Dict]:
1716
+ raise NotImplementedError("Swarm not supported in Podman")
1717
+
1718
+ def create_service(
1719
+ self,
1720
+ name: str,
1721
+ image: str,
1722
+ replicas: int = 1,
1723
+ ports: Optional[Dict[str, str]] = None,
1724
+ mounts: Optional[List[str]] = None,
1725
+ ) -> Dict:
1726
+ raise NotImplementedError("Swarm not supported in Podman")
1727
+
1728
+ def remove_service(self, service_id: str) -> Dict:
1729
+ raise NotImplementedError("Swarm not supported in Podman")
1730
+
1731
+
1732
+ def is_app_installed(app_name: str = "docker") -> bool:
1733
+ return shutil.which(app_name.lower()) is not None
1734
+
864
1735
 
865
1736
  def create_manager(
866
- manager_type: str, silent: bool = False, log_file: str = None
1737
+ manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
867
1738
  ) -> ContainerManagerBase:
868
- if manager_type.lower() == "docker" or manager_type.lower() == "swarm":
1739
+ if manager_type is None:
1740
+ manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
1741
+ if manager_type is None:
1742
+ if is_app_installed("podman"):
1743
+ manager_type = "podman"
1744
+ if is_app_installed("docker"):
1745
+ manager_type = "docker"
1746
+ if manager_type is None:
1747
+ raise ValueError(
1748
+ "No supported container manager detected. Set CONTAINER_MANAGER_TYPE or install Docker/Podman."
1749
+ )
1750
+ if manager_type.lower() in ["docker", "swarm"]:
869
1751
  return DockerManager(silent=silent, log_file=log_file)
870
1752
  elif manager_type.lower() == "podman":
871
1753
  return PodmanManager(silent=silent, log_file=log_file)
@@ -881,7 +1763,7 @@ Container Manager: A tool to manage containers with Docker, Podman, and Docker S
881
1763
  Usage:
882
1764
  -h | --help [ See usage for script ]
883
1765
  -s | --silent [ Suppress output ]
884
- -m | --manager <type> [ docker, podman, swarm; default: docker ]
1766
+ -m | --manager <type> [ docker, podman, swarm; default: auto-detect ]
885
1767
  --log-file <path> [ Log to specified file (default: container_manager.log in script dir) ]
886
1768
 
887
1769
  Actions:
@@ -893,6 +1775,8 @@ Actions:
893
1775
  --platform <plat> [ Platform, e.g., linux/amd64 ]
894
1776
  --remove-image <image> [ Remove image ]
895
1777
  --force [ Force removal (global for remove actions) ]
1778
+ --prune-images [ Prune unused images ]
1779
+ --all [ Prune all unused images ]
896
1780
  --list-containers [ List containers ]
897
1781
  --all [ Show all containers ]
898
1782
  --run-container <image> [ Run container ]
@@ -906,6 +1790,7 @@ Actions:
906
1790
  --timeout <sec> [ Timeout, default 10 ]
907
1791
  --remove-container <id>[ Remove container ]
908
1792
  --force [ Force ]
1793
+ --prune-containers [ Prune stopped containers ]
909
1794
  --get-container-logs <id> [ Get logs ]
910
1795
  --tail <tail> [ Tail lines, default all ]
911
1796
  --exec-in-container <id> [ Exec command ]
@@ -915,10 +1800,15 @@ Actions:
915
1800
  --create-volume <name> [ Create volume ]
916
1801
  --remove-volume <name> [ Remove volume ]
917
1802
  --force [ Force ]
1803
+ --prune-volumes [ Prune unused volumes ]
1804
+ --all [ Remove all volumes (dangerous) ]
918
1805
  --list-networks [ List networks ]
919
1806
  --create-network <name>[ Create network ]
920
1807
  --driver <driver> [ Driver, default bridge ]
921
1808
  --remove-network <id> [ Remove network ]
1809
+ --prune-networks [ Prune unused networks ]
1810
+ --prune-system [ Prune system resources ]
1811
+ --all [ Prune all unused (including volumes, build cache) ]
922
1812
  --compose-up <file> [ Compose up ]
923
1813
  --build [ Build images ]
924
1814
  --detach [ Detach mode, default true ]
@@ -945,263 +1835,194 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
945
1835
  )
946
1836
 
947
1837
 
948
- def main(argv):
949
- get_version = False
950
- get_info = False
951
- list_images = False
952
- pull_image = False
953
- pull_image_str = None
954
- tag = "latest"
955
- platform = None
956
- remove_image = False
957
- remove_image_str = None
958
- force = False
959
- list_containers = False
960
- all_containers = False
961
- run_container = False
962
- run_image = None
963
- name = None
964
- command = None
965
- detach = False
966
- ports_str = None
967
- volumes_str = None
968
- environment_str = None
969
- stop_container = False
970
- stop_container_id = None
971
- timeout = 10
972
- remove_container = False
973
- remove_container_id = None
974
- get_container_logs = False
975
- container_logs_id = None
976
- tail = "all"
977
- exec_in_container = False
978
- exec_container_id = None
979
- exec_command = None
980
- exec_detach = False
981
- list_volumes = False
982
- create_volume = False
983
- create_volume_name = None
984
- remove_volume = False
985
- remove_volume_name = None
986
- list_networks = False
987
- create_network = False
988
- create_network_name = None
989
- driver = "bridge"
990
- remove_network = False
991
- remove_network_id = None
992
- compose_up = False
993
- compose_up_file = None
994
- compose_build = False
995
- compose_detach = True
996
- compose_down = False
997
- compose_down_file = None
998
- compose_ps = False
999
- compose_ps_file = None
1000
- compose_logs = False
1001
- compose_logs_file = None
1002
- compose_service = None
1003
- init_swarm = False
1004
- advertise_addr = None
1005
- leave_swarm = False
1006
- list_nodes = False
1007
- list_services = False
1008
- create_service = False
1009
- create_service_name = None
1010
- service_image = None
1011
- replicas = 1
1012
- mounts_str = None
1013
- remove_service = False
1014
- remove_service_id = None
1015
- manager_type = "docker"
1016
- silent = False
1017
- log_file = None
1018
-
1019
- try:
1020
- opts, _ = getopt.getopt(
1021
- argv,
1022
- "hsm:",
1023
- [
1024
- "help",
1025
- "silent",
1026
- "manager=",
1027
- "log-file=",
1028
- "get-version",
1029
- "get-info",
1030
- "list-images",
1031
- "pull-image=",
1032
- "tag=",
1033
- "platform=",
1034
- "remove-image=",
1035
- "force",
1036
- "list-containers",
1037
- "all",
1038
- "run-container=",
1039
- "name=",
1040
- "command=",
1041
- "detach",
1042
- "ports=",
1043
- "volumes=",
1044
- "environment=",
1045
- "stop-container=",
1046
- "timeout=",
1047
- "remove-container=",
1048
- "get-container-logs=",
1049
- "tail=",
1050
- "exec-in-container=",
1051
- "exec-command=",
1052
- "exec-detach",
1053
- "list-volumes",
1054
- "create-volume=",
1055
- "remove-volume=",
1056
- "list-networks",
1057
- "create-network=",
1058
- "driver=",
1059
- "remove-network=",
1060
- "compose-up=",
1061
- "build",
1062
- "compose-down=",
1063
- "compose-ps=",
1064
- "compose-logs=",
1065
- "service=",
1066
- "init-swarm",
1067
- "advertise-addr=",
1068
- "leave-swarm",
1069
- "list-nodes",
1070
- "list-services",
1071
- "create-service=",
1072
- "image=",
1073
- "replicas=",
1074
- "mounts=",
1075
- "remove-service=",
1076
- ],
1077
- )
1078
- except getopt.GetoptError:
1079
- usage()
1080
- sys.exit(2)
1838
+ def container_manager():
1839
+ parser = argparse.ArgumentParser(
1840
+ description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
1841
+ )
1842
+ parser.add_argument("-s", "--silent", action="store_true", help="Suppress output")
1843
+ parser.add_argument(
1844
+ "-m",
1845
+ "--manager",
1846
+ type=str,
1847
+ default=None,
1848
+ help="Container manager type: docker, podman, swarm (default: auto-detect)",
1849
+ )
1850
+ parser.add_argument("--log-file", type=str, default=None, help="Path to log file")
1851
+ parser.add_argument("--get-version", action="store_true", help="Get version info")
1852
+ parser.add_argument("--get-info", action="store_true", help="Get system info")
1853
+ parser.add_argument("--list-images", action="store_true", help="List images")
1854
+ parser.add_argument("--pull-image", type=str, default=None, help="Image to pull")
1855
+ parser.add_argument("--tag", type=str, default="latest", help="Image tag")
1856
+ parser.add_argument("--platform", type=str, default=None, help="Platform")
1857
+ parser.add_argument(
1858
+ "--remove-image", type=str, default=None, help="Image to remove"
1859
+ )
1860
+ parser.add_argument("--prune-images", action="store_true", help="Prune images")
1861
+ parser.add_argument(
1862
+ "--list-containers", action="store_true", help="List containers"
1863
+ )
1864
+ parser.add_argument("--all", action="store_true", help="Show all containers")
1865
+ parser.add_argument("--run-container", type=str, default=None, help="Image to run")
1866
+ parser.add_argument("--name", type=str, default=None, help="Container name")
1867
+ parser.add_argument("--command", type=str, default=None, help="Command to run")
1868
+ parser.add_argument("--detach", action="store_true", help="Detach mode")
1869
+ parser.add_argument("--ports", type=str, default=None, help="Port mappings")
1870
+ parser.add_argument("--volumes", type=str, default=None, help="Volume mappings")
1871
+ parser.add_argument(
1872
+ "--environment", type=str, default=None, help="Environment vars"
1873
+ )
1874
+ parser.add_argument(
1875
+ "--stop-container", type=str, default=None, help="Container to stop"
1876
+ )
1877
+ parser.add_argument("--timeout", type=int, default=10, help="Timeout in seconds")
1878
+ parser.add_argument(
1879
+ "--remove-container", type=str, default=None, help="Container to remove"
1880
+ )
1881
+ parser.add_argument(
1882
+ "--prune-containers", action="store_true", help="Prune containers"
1883
+ )
1884
+ parser.add_argument(
1885
+ "--get-container-logs", type=str, default=None, help="Container logs"
1886
+ )
1887
+ parser.add_argument("--tail", type=str, default="all", help="Tail lines")
1888
+ parser.add_argument(
1889
+ "--exec-in-container", type=str, default=None, help="Container to exec"
1890
+ )
1891
+ parser.add_argument("--exec-command", type=str, default=None, help="Exec command")
1892
+ parser.add_argument("--exec-detach", action="store_true", help="Detach exec")
1893
+ parser.add_argument("--list-volumes", action="store_true", help="List volumes")
1894
+ parser.add_argument(
1895
+ "--create-volume", type=str, default=None, help="Volume to create"
1896
+ )
1897
+ parser.add_argument(
1898
+ "--remove-volume", type=str, default=None, help="Volume to remove"
1899
+ )
1900
+ parser.add_argument("--prune-volumes", action="store_true", help="Prune volumes")
1901
+ parser.add_argument("--list-networks", action="store_true", help="List networks")
1902
+ parser.add_argument(
1903
+ "--create-network", type=str, default=None, help="Network to create"
1904
+ )
1905
+ parser.add_argument("--driver", type=str, default="bridge", help="Network driver")
1906
+ parser.add_argument(
1907
+ "--remove-network", type=str, default=None, help="Network to remove"
1908
+ )
1909
+ parser.add_argument("--prune-networks", action="store_true", help="Prune networks")
1910
+ parser.add_argument("--prune-system", action="store_true", help="Prune system")
1911
+ parser.add_argument("--compose-up", type=str, default=None, help="Compose file up")
1912
+ parser.add_argument("--build", action="store_true", help="Build images")
1913
+ parser.add_argument(
1914
+ "--compose-detach", action="store_true", default=True, help="Detach compose"
1915
+ )
1916
+ parser.add_argument(
1917
+ "--compose-down", type=str, default=None, help="Compose file down"
1918
+ )
1919
+ parser.add_argument("--compose-ps", type=str, default=None, help="Compose ps")
1920
+ parser.add_argument("--compose-logs", type=str, default=None, help="Compose logs")
1921
+ parser.add_argument("--service", type=str, default=None, help="Specific service")
1922
+ parser.add_argument("--init-swarm", action="store_true", help="Init swarm")
1923
+ parser.add_argument(
1924
+ "--advertise-addr", type=str, default=None, help="Advertise address"
1925
+ )
1926
+ parser.add_argument("--leave-swarm", action="store_true", help="Leave swarm")
1927
+ parser.add_argument("--list-nodes", action="store_true", help="List swarm nodes")
1928
+ parser.add_argument(
1929
+ "--list-services", action="store_true", help="List swarm services"
1930
+ )
1931
+ parser.add_argument(
1932
+ "--create-service", type=str, default=None, help="Service to create"
1933
+ )
1934
+ parser.add_argument("--image", type=str, default=None, help="Service image")
1935
+ parser.add_argument("--replicas", type=int, default=1, help="Replicas")
1936
+ parser.add_argument("--mounts", type=str, default=None, help="Mounts")
1937
+ parser.add_argument(
1938
+ "--remove-service", type=str, default=None, help="Service to remove"
1939
+ )
1940
+ parser.add_argument("--force", action="store_true", help="Force removal")
1941
+ parser.add_argument("-h", "--help", action="store_true", help="Show help")
1942
+
1943
+ args = parser.parse_args()
1081
1944
 
1082
- for opt, arg in opts:
1083
- if opt in ("-h", "--help"):
1084
- usage()
1085
- sys.exit()
1086
- elif opt in ("-s", "--silent"):
1087
- silent = True
1088
- elif opt in ("-m", "--manager"):
1089
- manager_type = arg
1090
- elif opt == "--log-file":
1091
- log_file = arg
1092
- elif opt == "--get-version":
1093
- get_version = True
1094
- elif opt == "--get-info":
1095
- get_info = True
1096
- elif opt == "--list-images":
1097
- list_images = True
1098
- elif opt == "--pull-image":
1099
- pull_image = True
1100
- pull_image_str = arg
1101
- elif opt == "--tag":
1102
- tag = arg
1103
- elif opt == "--platform":
1104
- platform = arg
1105
- elif opt == "--remove-image":
1106
- remove_image = True
1107
- remove_image_str = arg
1108
- elif opt == "--force":
1109
- force = True
1110
- elif opt == "--list-containers":
1111
- list_containers = True
1112
- elif opt == "--all":
1113
- all_containers = True
1114
- elif opt == "--run-container":
1115
- run_container = True
1116
- run_image = arg
1117
- elif opt == "--name":
1118
- name = arg
1119
- elif opt == "--command":
1120
- command = arg
1121
- elif opt == "--detach":
1122
- detach = True
1123
- elif opt == "--ports":
1124
- ports_str = arg
1125
- elif opt == "--volumes":
1126
- volumes_str = arg
1127
- elif opt == "--environment":
1128
- environment_str = arg
1129
- elif opt == "--stop-container":
1130
- stop_container = True
1131
- stop_container_id = arg
1132
- elif opt == "--timeout":
1133
- timeout = int(arg)
1134
- elif opt == "--remove-container":
1135
- remove_container = True
1136
- remove_container_id = arg
1137
- elif opt == "--get-container-logs":
1138
- get_container_logs = True
1139
- container_logs_id = arg
1140
- elif opt == "--tail":
1141
- tail = arg
1142
- elif opt == "--exec-in-container":
1143
- exec_in_container = True
1144
- exec_container_id = arg
1145
- elif opt == "--exec-command":
1146
- exec_command = arg
1147
- elif opt == "--exec-detach":
1148
- exec_detach = True
1149
- elif opt == "--list-volumes":
1150
- list_volumes = True
1151
- elif opt == "--create-volume":
1152
- create_volume = True
1153
- create_volume_name = arg
1154
- elif opt == "--remove-volume":
1155
- remove_volume = True
1156
- remove_volume_name = arg
1157
- elif opt == "--list-networks":
1158
- list_networks = True
1159
- elif opt == "--create-network":
1160
- create_network = True
1161
- create_network_name = arg
1162
- elif opt == "--driver":
1163
- driver = arg
1164
- elif opt == "--remove-network":
1165
- remove_network = True
1166
- remove_network_id = arg
1167
- elif opt == "--compose-up":
1168
- compose_up = True
1169
- compose_up_file = arg
1170
- elif opt == "--build":
1171
- compose_build = True
1172
- elif opt == "--compose-down":
1173
- compose_down = True
1174
- compose_down_file = arg
1175
- elif opt == "--compose-ps":
1176
- compose_ps = True
1177
- compose_ps_file = arg
1178
- elif opt == "--compose-logs":
1179
- compose_logs = True
1180
- compose_logs_file = arg
1181
- elif opt == "--service":
1182
- compose_service = arg
1183
- elif opt == "--init-swarm":
1184
- init_swarm = True
1185
- elif opt == "--advertise-addr":
1186
- advertise_addr = arg
1187
- elif opt == "--leave-swarm":
1188
- leave_swarm = True
1189
- elif opt == "--list-nodes":
1190
- list_nodes = True
1191
- elif opt == "--list-services":
1192
- list_services = True
1193
- elif opt == "--create-service":
1194
- create_service = True
1195
- create_service_name = arg
1196
- elif opt == "--image":
1197
- service_image = arg
1198
- elif opt == "--replicas":
1199
- replicas = int(arg)
1200
- elif opt == "--mounts":
1201
- mounts_str = arg
1202
- elif opt == "--remove-service":
1203
- remove_service = True
1204
- remove_service_id = arg
1945
+ if args.help:
1946
+ usage()
1947
+ sys.exit(0)
1948
+
1949
+ get_version = args.get_version
1950
+ get_info = args.get_info
1951
+ list_images = args.list_images
1952
+ pull_image = args.pull_image is not None
1953
+ pull_image_str = args.pull_image
1954
+ tag = args.tag
1955
+ platform = args.platform
1956
+ remove_image = args.remove_image is not None
1957
+ remove_image_str = args.remove_image
1958
+ prune_images = args.prune_images
1959
+ prune_images_all = args.all if prune_images else False
1960
+ force = args.force
1961
+ list_containers = args.list_containers
1962
+ all_containers = args.all if list_containers else False
1963
+ run_container = args.run_container is not None
1964
+ run_image = args.run_container
1965
+ name = args.name
1966
+ command = args.command
1967
+ detach = args.detach
1968
+ ports_str = args.ports
1969
+ volumes_str = args.volumes
1970
+ environment_str = args.environment
1971
+ stop_container = args.stop_container is not None
1972
+ stop_container_id = args.stop_container
1973
+ timeout = args.timeout
1974
+ remove_container = args.remove_container is not None
1975
+ remove_container_id = args.remove_container
1976
+ prune_containers = args.prune_containers
1977
+ get_container_logs = args.get_container_logs is not None
1978
+ container_logs_id = args.get_container_logs
1979
+ tail = args.tail
1980
+ exec_in_container = args.exec_in_container is not None
1981
+ exec_container_id = args.exec_in_container
1982
+ exec_command = args.exec_command
1983
+ exec_detach = args.exec_detach
1984
+ list_volumes = args.list_volumes
1985
+ create_volume = args.create_volume is not None
1986
+ create_volume_name = args.create_volume
1987
+ remove_volume = args.remove_volume is not None
1988
+ remove_volume_name = args.remove_volume
1989
+ prune_volumes = args.prune_volumes
1990
+ prune_volumes_all = args.all if prune_volumes else False
1991
+ list_networks = args.list_networks
1992
+ create_network = args.create_network is not None
1993
+ create_network_name = args.create_network
1994
+ driver = args.driver
1995
+ remove_network = args.remove_network is not None
1996
+ remove_network_id = args.remove_network
1997
+ prune_networks = args.prune_networks
1998
+ prune_system = args.prune_system
1999
+ prune_system_all = args.all if prune_system else False
2000
+ compose_up = args.compose_up is not None
2001
+ compose_up_file = args.compose_up
2002
+ compose_build = args.build
2003
+ compose_detach = args.compose_detach
2004
+ compose_down = args.compose_down is not None
2005
+ compose_down_file = args.compose_down
2006
+ compose_ps = args.compose_ps is not None
2007
+ compose_ps_file = args.compose_ps
2008
+ compose_logs = args.compose_logs is not None
2009
+ compose_logs_file = args.compose_logs
2010
+ compose_service = args.service
2011
+ init_swarm = args.init_swarm
2012
+ advertise_addr = args.advertise_addr
2013
+ leave_swarm = args.leave_swarm
2014
+ list_nodes = args.list_nodes
2015
+ list_services = args.list_services
2016
+ create_service = args.create_service is not None
2017
+ create_service_name = args.create_service
2018
+ service_image = args.image
2019
+ replicas = args.replicas
2020
+ mounts_str = args.mounts
2021
+ remove_service = args.remove_service is not None
2022
+ remove_service_id = args.remove_service
2023
+ manager_type = args.manager
2024
+ silent = args.silent
2025
+ log_file = args.log_file
1205
2026
 
1206
2027
  manager = create_manager(manager_type, silent, log_file)
1207
2028
 
@@ -1224,6 +2045,9 @@ def main(argv):
1224
2045
  raise ValueError("Image required for remove-image")
1225
2046
  print(json.dumps(manager.remove_image(remove_image_str, force), indent=2))
1226
2047
 
2048
+ if prune_images:
2049
+ print(json.dumps(manager.prune_images(force, prune_images_all), indent=2))
2050
+
1227
2051
  if list_containers:
1228
2052
  print(json.dumps(manager.list_containers(all_containers), indent=2))
1229
2053
 
@@ -1269,6 +2093,9 @@ def main(argv):
1269
2093
  json.dumps(manager.remove_container(remove_container_id, force), indent=2)
1270
2094
  )
1271
2095
 
2096
+ if prune_containers:
2097
+ print(json.dumps(manager.prune_containers(), indent=2))
2098
+
1272
2099
  if get_container_logs:
1273
2100
  if not container_logs_id:
1274
2101
  raise ValueError("Container ID required for get-container-logs")
@@ -1298,6 +2125,9 @@ def main(argv):
1298
2125
  raise ValueError("Name required for remove-volume")
1299
2126
  print(json.dumps(manager.remove_volume(remove_volume_name, force), indent=2))
1300
2127
 
2128
+ if prune_volumes:
2129
+ print(json.dumps(manager.prune_volumes(force, prune_volumes_all), indent=2))
2130
+
1301
2131
  if list_networks:
1302
2132
  print(json.dumps(manager.list_networks(), indent=2))
1303
2133
 
@@ -1311,6 +2141,12 @@ def main(argv):
1311
2141
  raise ValueError("ID required for remove-network")
1312
2142
  print(json.dumps(manager.remove_network(remove_network_id), indent=2))
1313
2143
 
2144
+ if prune_networks:
2145
+ print(json.dumps(manager.prune_networks(), indent=2))
2146
+
2147
+ if prune_system:
2148
+ print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
2149
+
1314
2150
  if compose_up:
1315
2151
  if not compose_up_file:
1316
2152
  raise ValueError("File required for compose-up")
@@ -1378,4 +2214,4 @@ if __name__ == "__main__":
1378
2214
  if len(sys.argv) < 2:
1379
2215
  usage()
1380
2216
  sys.exit(2)
1381
- main(sys.argv[1:])
2217
+ container_manager()