container-manager-mcp 1.0.3__py3-none-any.whl → 1.0.4__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.
@@ -8,10 +8,12 @@ from abc import ABC, abstractmethod
8
8
  from typing import List, Dict, Optional, Any
9
9
  import argparse
10
10
  import json
11
+ import shutil
11
12
  import subprocess
12
13
  from datetime import datetime
13
14
  import dateutil.parser
14
15
  import platform
16
+ import traceback
15
17
 
16
18
  try:
17
19
  import docker
@@ -40,7 +42,7 @@ class ContainerManagerBase(ABC):
40
42
  log_file = os.path.join(script_dir, "container_manager.log")
41
43
  logging.basicConfig(
42
44
  filename=log_file,
43
- level=logging.INFO,
45
+ level=logging.DEBUG, # Changed to DEBUG for more detailed logging
44
46
  format="%(asctime)s - %(levelname)s - %(message)s",
45
47
  )
46
48
  self.logger = logging.getLogger(__name__)
@@ -57,7 +59,10 @@ class ContainerManagerBase(ABC):
57
59
  if result:
58
60
  self.logger.info(f"Result: {result}")
59
61
  if error:
60
- self.logger.error(f"Error: {str(error)}")
62
+ self.logger.error(
63
+ f"Error in {action}: {type(error).__name__}: {str(error)}"
64
+ )
65
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
61
66
 
62
67
  def _format_size(self, size_bytes: int) -> str:
63
68
  """Helper to format bytes to human-readable (e.g., 1.23GB)."""
@@ -135,7 +140,7 @@ class ContainerManagerBase(ABC):
135
140
  pass
136
141
 
137
142
  @abstractmethod
138
- def prune_containers(self, force: bool = False) -> Dict:
143
+ def prune_containers(self) -> Dict:
139
144
  pass
140
145
 
141
146
  @abstractmethod
@@ -245,6 +250,78 @@ class DockerManager(ContainerManagerBase):
245
250
  self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
246
251
  raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
247
252
 
253
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
254
+ params = {"force": force, "all": all}
255
+ try:
256
+ filters = {"until": None} if all else {}
257
+ result = self.client.system.prune(filters=filters, volumes=all)
258
+ if result is None:
259
+ result = {
260
+ "SpaceReclaimed": 0,
261
+ "ImagesDeleted": [],
262
+ "ContainersDeleted": [],
263
+ "VolumesDeleted": [],
264
+ "NetworksDeleted": [],
265
+ }
266
+ self.logger.debug(f"Raw prune_system result: {result}")
267
+ pruned = {
268
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
269
+ "images_removed": (
270
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
271
+ ),
272
+ "containers_removed": (
273
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
274
+ ),
275
+ "volumes_removed": (
276
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
277
+ ),
278
+ "networks_removed": (
279
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
280
+ ),
281
+ }
282
+ self.log_action("prune_system", params, pruned)
283
+ return pruned
284
+ except Exception as e:
285
+ self.log_action("prune_system", params, error=e)
286
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
287
+
288
+ # Other DockerManager methods remain unchanged (omitted for brevity)
289
+ def get_version(self) -> Dict:
290
+ params = {}
291
+ try:
292
+ version = self.client.version()
293
+ result = {
294
+ "version": version.get("Version", "unknown"),
295
+ "api_version": version.get("ApiVersion", "unknown"),
296
+ "os": version.get("Os", "unknown"),
297
+ "arch": version.get("Arch", "unknown"),
298
+ "build_time": version.get("BuildTime", "unknown"),
299
+ }
300
+ self.log_action("get_version", params, result)
301
+ return result
302
+ except Exception as e:
303
+ self.log_action("get_version", params, error=e)
304
+ raise RuntimeError(f"Failed to get version: {str(e)}")
305
+
306
+ def get_info(self) -> Dict:
307
+ params = {}
308
+ try:
309
+ info = self.client.info()
310
+ result = {
311
+ "containers_total": info.get("Containers", 0),
312
+ "containers_running": info.get("ContainersRunning", 0),
313
+ "images": info.get("Images", 0),
314
+ "driver": info.get("Driver", "unknown"),
315
+ "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
316
+ "memory_total": self._format_size(info.get("MemTotal", 0)),
317
+ "swap_total": self._format_size(info.get("SwapTotal", 0)),
318
+ }
319
+ self.log_action("get_info", params, result)
320
+ return result
321
+ except Exception as e:
322
+ self.log_action("get_info", params, error=e)
323
+ raise RuntimeError(f"Failed to get info: {str(e)}")
324
+
248
325
  def list_images(self) -> List[Dict]:
249
326
  params = {}
250
327
  try:
@@ -314,21 +391,55 @@ class DockerManager(ContainerManagerBase):
314
391
  self.log_action("pull_image", params, error=e)
315
392
  raise RuntimeError(f"Failed to pull image: {str(e)}")
316
393
 
394
+ def remove_image(self, image: str, force: bool = False) -> Dict:
395
+ params = {"image": image, "force": force}
396
+ try:
397
+ self.client.images.remove(image, force=force)
398
+ result = {"removed": image}
399
+ self.log_action("remove_image", params, result)
400
+ return result
401
+ except Exception as e:
402
+ self.log_action("remove_image", params, error=e)
403
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
404
+
317
405
  def prune_images(self, force: bool = False, all: bool = False) -> Dict:
318
406
  params = {"force": force, "all": all}
319
407
  try:
320
- filters = {"dangling": {"true": True}} if not all else {}
321
- result = self.client.images.prune(filters=filters)
322
- pruned = {
323
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
324
- "images_removed": (
325
- [img["Id"][7:19] for img in result["ImagesRemoved"]]
326
- if result["ImagesRemoved"]
327
- else []
328
- ),
329
- }
330
- self.log_action("prune_images", params, pruned)
331
- return pruned
408
+ if all:
409
+ # Manually remove all unused images
410
+ images = self.client.images.list(all=True)
411
+ removed = []
412
+ for img in images:
413
+ try:
414
+ for tag in img.attrs.get("RepoTags", []):
415
+ self.client.images.remove(tag, force=force)
416
+ removed.append(img.attrs["Id"][7:19])
417
+ except Exception as e:
418
+ self.logger.info(
419
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
420
+ )
421
+ continue
422
+ result = {
423
+ "images_removed": removed,
424
+ "space_reclaimed": "N/A (all images)",
425
+ }
426
+ else:
427
+ filters = {"dangling": True} if not all else {}
428
+ result = self.client.images.prune(filters=filters)
429
+ if result is None:
430
+ result = {"SpaceReclaimed": 0, "ImagesDeleted": []}
431
+ self.logger.debug(f"Raw prune_images result: {result}")
432
+ pruned = {
433
+ "space_reclaimed": self._format_size(
434
+ result.get("SpaceReclaimed", 0)
435
+ ),
436
+ "images_removed": (
437
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
438
+ ),
439
+ }
440
+ result = pruned
441
+ self.log_action("prune_images", params, result)
442
+ return result
332
443
  except Exception as e:
333
444
  self.log_action("prune_images", params, error=e)
334
445
  raise RuntimeError(f"Failed to prune images: {str(e)}")
@@ -423,134 +534,6 @@ class DockerManager(ContainerManagerBase):
423
534
  self.log_action("run_container", params, error=e)
424
535
  raise RuntimeError(f"Failed to run container: {str(e)}")
425
536
 
426
- def prune_containers(self, force: bool = False) -> Dict:
427
- params = {"force": force}
428
- try:
429
- result = self.client.containers.prune()
430
- pruned = {
431
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
432
- "containers_removed": (
433
- [c["Id"][7:19] for c in result["ContainersRemoved"]]
434
- if result["ContainersRemoved"]
435
- else []
436
- ),
437
- }
438
- self.log_action("prune_containers", params, pruned)
439
- return pruned
440
- except Exception as e:
441
- self.log_action("prune_containers", params, error=e)
442
- raise RuntimeError(f"Failed to prune containers: {str(e)}")
443
-
444
- def list_networks(self) -> List[Dict]:
445
- params = {}
446
- try:
447
- networks = self.client.networks.list()
448
- result = []
449
- for net in networks:
450
- attrs = net.attrs
451
- containers = len(attrs.get("Containers", {}))
452
- created = attrs.get("Created", None)
453
- created_str = self._parse_timestamp(created)
454
- simplified = {
455
- "id": attrs.get("Id", "unknown")[7:19],
456
- "name": attrs.get("Name", "unknown"),
457
- "driver": attrs.get("Driver", "unknown"),
458
- "scope": attrs.get("Scope", "unknown"),
459
- "containers": containers,
460
- "created": created_str,
461
- }
462
- result.append(simplified)
463
- self.log_action("list_networks", params, result)
464
- return result
465
- except Exception as e:
466
- self.log_action("list_networks", params, error=e)
467
- raise RuntimeError(f"Failed to list networks: {str(e)}")
468
-
469
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
470
- params = {"name": name, "driver": driver}
471
- try:
472
- network = self.client.networks.create(name, driver=driver)
473
- attrs = network.attrs
474
- created = attrs.get("Created", None)
475
- created_str = self._parse_timestamp(created)
476
- result = {
477
- "id": attrs.get("Id", "unknown")[7:19],
478
- "name": attrs.get("Name", name),
479
- "driver": attrs.get("Driver", driver),
480
- "scope": attrs.get("Scope", "unknown"),
481
- "created": created_str,
482
- }
483
- self.log_action("create_network", params, result)
484
- return result
485
- except Exception as e:
486
- self.log_action("create_network", params, error=e)
487
- raise RuntimeError(f"Failed to create network: {str(e)}")
488
-
489
- def prune_networks(self) -> Dict:
490
- params = {}
491
- try:
492
- result = self.client.networks.prune()
493
- pruned = {
494
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
495
- "networks_removed": (
496
- [n["Id"][7:19] for n in result["NetworksRemoved"]]
497
- if result["NetworksRemoved"]
498
- else []
499
- ),
500
- }
501
- self.log_action("prune_networks", params, pruned)
502
- return pruned
503
- except Exception as e:
504
- self.log_action("prune_networks", params, error=e)
505
- raise RuntimeError(f"Failed to prune networks: {str(e)}")
506
-
507
- def get_version(self) -> Dict:
508
- params = {}
509
- try:
510
- version = self.client.version()
511
- result = {
512
- "version": version.get("Version", "unknown"),
513
- "api_version": version.get("ApiVersion", "unknown"),
514
- "os": version.get("Os", "unknown"),
515
- "arch": version.get("Arch", "unknown"),
516
- "build_time": version.get("BuildTime", "unknown"),
517
- }
518
- self.log_action("get_version", params, result)
519
- return result
520
- except Exception as e:
521
- self.log_action("get_version", params, error=e)
522
- raise RuntimeError(f"Failed to get version: {str(e)}")
523
-
524
- def get_info(self) -> Dict:
525
- params = {}
526
- try:
527
- info = self.client.info()
528
- result = {
529
- "containers_total": info.get("Containers", 0),
530
- "containers_running": info.get("ContainersRunning", 0),
531
- "images": info.get("Images", 0),
532
- "driver": info.get("Driver", "unknown"),
533
- "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
534
- "memory_total": self._format_size(info.get("MemTotal", 0)),
535
- "swap_total": self._format_size(info.get("SwapTotal", 0)),
536
- }
537
- self.log_action("get_info", params, result)
538
- return result
539
- except Exception as e:
540
- self.log_action("get_info", params, error=e)
541
- raise RuntimeError(f"Failed to get info: {str(e)}")
542
-
543
- def remove_image(self, image: str, force: bool = False) -> Dict:
544
- params = {"image": image, "force": force}
545
- try:
546
- self.client.images.remove(image, force=force)
547
- result = {"removed": image}
548
- self.log_action("remove_image", params, result)
549
- return result
550
- except Exception as e:
551
- self.log_action("remove_image", params, error=e)
552
- raise RuntimeError(f"Failed to remove image: {str(e)}")
553
-
554
537
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
555
538
  params = {"container_id": container_id, "timeout": timeout}
556
539
  try:
@@ -575,6 +558,34 @@ class DockerManager(ContainerManagerBase):
575
558
  self.log_action("remove_container", params, error=e)
576
559
  raise RuntimeError(f"Failed to remove container: {str(e)}")
577
560
 
561
+ def prune_containers(self) -> Dict:
562
+ params = {}
563
+ try:
564
+ result = self.client.containers.prune()
565
+ self.logger.debug(f"Raw prune_containers result: {result}")
566
+ if result is None:
567
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
568
+ pruned = {
569
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
570
+ "containers_removed": (
571
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
572
+ ),
573
+ }
574
+ self.log_action("prune_containers", params, pruned)
575
+ return pruned
576
+ except TypeError as e:
577
+ self.logger.error(f"TypeError in prune_containers: {str(e)}")
578
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
579
+ self.log_action("prune_containers", params, error=e)
580
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
581
+ except Exception as e:
582
+ self.logger.error(
583
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
584
+ )
585
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
586
+ self.log_action("prune_containers", params, error=e)
587
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
588
+
578
589
  def get_container_logs(self, container_id: str, tail: str = "all") -> str:
579
590
  params = {"container_id": container_id, "tail": tail}
580
591
  try:
@@ -660,7 +671,6 @@ class DockerManager(ContainerManagerBase):
660
671
  params = {"force": force, "all": all}
661
672
  try:
662
673
  if all:
663
- # Remove all volumes (equivalent to --all, but docker doesn't have --all for prune; we list and remove)
664
674
  volumes = self.client.volumes.list(all=True)
665
675
  removed = []
666
676
  for v in volumes:
@@ -668,20 +678,25 @@ class DockerManager(ContainerManagerBase):
668
678
  v.remove(force=force)
669
679
  removed.append(v.attrs["Name"])
670
680
  except Exception as e:
671
- logging.info(f"Info: {e}")
672
- pass
681
+ self.logger.info(
682
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
683
+ )
684
+ continue
673
685
  result = {
674
686
  "volumes_removed": removed,
675
687
  "space_reclaimed": "N/A (all volumes)",
676
688
  }
677
689
  else:
678
690
  result = self.client.volumes.prune()
691
+ if result is None:
692
+ result = {"SpaceReclaimed": 0, "VolumesDeleted": []}
693
+ self.logger.debug(f"Raw prune_volumes result: {result}")
679
694
  pruned = {
680
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
695
+ "space_reclaimed": self._format_size(
696
+ result.get("SpaceReclaimed", 0)
697
+ ),
681
698
  "volumes_removed": (
682
- [v["Name"] for v in result["VolumesRemoved"]]
683
- if result["VolumesRemoved"]
684
- else []
699
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
685
700
  ),
686
701
  }
687
702
  result = pruned
@@ -691,46 +706,81 @@ class DockerManager(ContainerManagerBase):
691
706
  self.log_action("prune_volumes", params, error=e)
692
707
  raise RuntimeError(f"Failed to prune volumes: {str(e)}")
693
708
 
694
- def remove_network(self, network_id: str) -> Dict:
695
- params = {"network_id": network_id}
696
- try:
697
- network = self.client.networks.get(network_id)
698
- network.remove()
699
- result = {"removed": network_id}
700
- self.log_action("remove_network", params, result)
701
- return result
702
- except Exception as e:
703
- self.log_action("remove_network", params, error=e)
704
- raise RuntimeError(f"Failed to remove network: {str(e)}")
705
-
706
- def prune_system(self, force: bool = False, all: bool = False) -> Dict:
707
- params = {"force": force, "all": all}
709
+ def list_networks(self) -> List[Dict]:
710
+ params = {}
708
711
  try:
709
- filters = {"until": None} if all else {}
710
- result = self.client.system.prune(filters=filters, space=True)
711
- pruned = {
712
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
713
- "images_removed": (
714
- [img["Id"][7:19] for img in result["ImagesDeleted"]]
715
- if "ImagesDeleted" in result
716
- else []
717
- ),
718
- "containers_removed": (
719
- [c["Id"][7:19] for c in result["ContainersDeleted"]]
720
- if "ContainersDeleted" in result
721
- else []
722
- ),
723
- "volumes_removed": (
724
- [v["Name"] for v in result["VolumesDeleted"]]
725
- if "VolumesDeleted" in result
726
- else []
712
+ networks = self.client.networks.list()
713
+ result = []
714
+ for net in networks:
715
+ attrs = net.attrs
716
+ containers = len(attrs.get("Containers", {}))
717
+ created = attrs.get("Created", None)
718
+ created_str = self._parse_timestamp(created)
719
+ simplified = {
720
+ "id": attrs.get("Id", "unknown")[7:19],
721
+ "name": attrs.get("Name", "unknown"),
722
+ "driver": attrs.get("Driver", "unknown"),
723
+ "scope": attrs.get("Scope", "unknown"),
724
+ "containers": containers,
725
+ "created": created_str,
726
+ }
727
+ result.append(simplified)
728
+ self.log_action("list_networks", params, result)
729
+ return result
730
+ except Exception as e:
731
+ self.log_action("list_networks", params, error=e)
732
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
733
+
734
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
735
+ params = {"name": name, "driver": driver}
736
+ try:
737
+ network = self.client.networks.create(name, driver=driver)
738
+ attrs = network.attrs
739
+ created = attrs.get("Created", None)
740
+ created_str = self._parse_timestamp(created)
741
+ result = {
742
+ "id": attrs.get("Id", "unknown")[7:19],
743
+ "name": attrs.get("Name", name),
744
+ "driver": attrs.get("Driver", driver),
745
+ "scope": attrs.get("Scope", "unknown"),
746
+ "created": created_str,
747
+ }
748
+ self.log_action("create_network", params, result)
749
+ return result
750
+ except Exception as e:
751
+ self.log_action("create_network", params, error=e)
752
+ raise RuntimeError(f"Failed to create network: {str(e)}")
753
+
754
+ def remove_network(self, network_id: str) -> Dict:
755
+ params = {"network_id": network_id}
756
+ try:
757
+ network = self.client.networks.get(network_id)
758
+ network.remove()
759
+ result = {"removed": network_id}
760
+ self.log_action("remove_network", params, result)
761
+ return result
762
+ except Exception as e:
763
+ self.log_action("remove_network", params, error=e)
764
+ raise RuntimeError(f"Failed to remove network: {str(e)}")
765
+
766
+ def prune_networks(self) -> Dict:
767
+ params = {}
768
+ try:
769
+ result = self.client.networks.prune()
770
+ if result is None:
771
+ result = {"SpaceReclaimed": 0, "NetworksDeleted": []}
772
+ self.logger.debug(f"Raw prune_networks result: {result}")
773
+ pruned = {
774
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
775
+ "networks_removed": (
776
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
727
777
  ),
728
778
  }
729
- self.log_action("prune_system", params, pruned)
779
+ self.log_action("prune_networks", params, pruned)
730
780
  return pruned
731
781
  except Exception as e:
732
- self.log_action("prune_system", params, error=e)
733
- raise RuntimeError(f"Failed to prune system: {str(e)}")
782
+ self.log_action("prune_networks", params, error=e)
783
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
734
784
 
735
785
  def compose_up(
736
786
  self, compose_file: str, detach: bool = True, build: bool = False
@@ -957,17 +1007,14 @@ class DockerManager(ContainerManagerBase):
957
1007
  class PodmanManager(ContainerManagerBase):
958
1008
  def __init__(self, silent: bool = False, log_file: Optional[str] = None):
959
1009
  super().__init__(silent, log_file)
960
-
961
1010
  if PodmanClient is None:
962
1011
  raise ImportError("Please install podman-py: pip install podman")
963
-
964
1012
  base_url = self._autodetect_podman_url()
965
1013
  if base_url is None:
966
1014
  self.logger.error(
967
1015
  "No valid Podman socket found after trying all known locations"
968
1016
  )
969
1017
  raise RuntimeError("Failed to connect to Podman: No valid socket found")
970
-
971
1018
  try:
972
1019
  self.client = PodmanClient(base_url=base_url)
973
1020
  self.logger.info(f"Connected to Podman with base_url: {base_url}")
@@ -1002,7 +1049,6 @@ class PodmanManager(ContainerManagerBase):
1002
1049
  """Attempt to connect to Podman with the given base_url."""
1003
1050
  try:
1004
1051
  client = PodmanClient(base_url=base_url)
1005
- # Test connection
1006
1052
  client.version()
1007
1053
  return client
1008
1054
  except PodmanError as e:
@@ -1011,41 +1057,31 @@ class PodmanManager(ContainerManagerBase):
1011
1057
 
1012
1058
  def _autodetect_podman_url(self) -> Optional[str]:
1013
1059
  """Autodetect the appropriate Podman socket URL based on platform."""
1014
- # Check for environment variable override
1015
1060
  base_url = os.environ.get("PODMAN_BASE_URL")
1016
1061
  if base_url:
1017
1062
  self.logger.info(f"Using PODMAN_BASE_URL from environment: {base_url}")
1018
1063
  return base_url
1019
-
1020
1064
  system = platform.system()
1021
1065
  is_wsl = self._is_wsl()
1022
-
1023
- # Define socket candidates based on platform
1024
1066
  socket_candidates = []
1025
1067
  if system == "Windows" and not is_wsl:
1026
- # Windows with Podman machine
1027
1068
  if self._is_podman_machine_running():
1028
1069
  socket_candidates.append("npipe:////./pipe/docker_engine")
1029
- # Fallback to WSL2 distro sockets if running in a mixed setup
1030
1070
  socket_candidates.extend(
1031
1071
  [
1032
- "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock", # Rootless
1033
- "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock", # Rootful
1072
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
1073
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
1034
1074
  ]
1035
1075
  )
1036
1076
  elif system == "Linux" or is_wsl:
1037
- # Linux or WSL2 distro: prioritize rootless, then rootful
1038
1077
  uid = os.getuid()
1039
1078
  socket_candidates.extend(
1040
1079
  [
1041
- f"unix:///run/user/{uid}/podman/podman.sock", # Rootless
1042
- "unix:///run/podman/podman.sock", # Rootful
1080
+ f"unix:///run/user/{uid}/podman/podman.sock",
1081
+ "unix:///run/podman/podman.sock",
1043
1082
  ]
1044
1083
  )
1045
-
1046
- # Try each socket candidate
1047
1084
  for url in socket_candidates:
1048
- # For Unix sockets, check if the file exists (on Linux/WSL2)
1049
1085
  if url.startswith("unix://") and (system == "Linux" or is_wsl):
1050
1086
  socket_path = url.replace("unix://", "")
1051
1087
  if not os.path.exists(socket_path):
@@ -1054,9 +1090,207 @@ class PodmanManager(ContainerManagerBase):
1054
1090
  client = self._try_connect(url)
1055
1091
  if client:
1056
1092
  return url
1057
-
1058
1093
  return None
1059
1094
 
1095
+ def prune_images(self, force: bool = False, all: bool = False) -> Dict:
1096
+ params = {"force": force, "all": all}
1097
+ try:
1098
+ if all:
1099
+ # Manually remove all unused images
1100
+ images = self.client.images.list(all=True)
1101
+ removed = []
1102
+ for img in images:
1103
+ try:
1104
+ for tag in img.attrs.get("Names", []):
1105
+ self.client.images.remove(tag, force=force)
1106
+ removed.append(img.attrs["Id"][7:19])
1107
+ except Exception as e:
1108
+ self.logger.info(
1109
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
1110
+ )
1111
+ continue
1112
+ result = {
1113
+ "images_removed": removed,
1114
+ "space_reclaimed": "N/A (all images)",
1115
+ }
1116
+ else:
1117
+ filters = {"dangling": True} if not all else {}
1118
+ result = self.client.images.prune(filters=filters)
1119
+ if result is None:
1120
+ result = {"SpaceReclaimed": 0, "ImagesRemoved": []}
1121
+ self.logger.debug(f"Raw prune_images result: {result}")
1122
+ pruned = {
1123
+ "space_reclaimed": self._format_size(
1124
+ result.get("SpaceReclaimed", 0)
1125
+ ),
1126
+ "images_removed": (
1127
+ [img["Id"][7:19] for img in result.get("ImagesRemoved", [])]
1128
+ or [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
1129
+ ),
1130
+ }
1131
+ result = pruned
1132
+ self.log_action("prune_images", params, result)
1133
+ return result
1134
+ except Exception as e:
1135
+ self.log_action("prune_images", params, error=e)
1136
+ raise RuntimeError(f"Failed to prune images: {str(e)}")
1137
+
1138
+ def prune_containers(self) -> Dict:
1139
+ params = {}
1140
+ try:
1141
+ result = self.client.containers.prune()
1142
+ self.logger.debug(f"Raw prune_containers result: {result}")
1143
+ if result is None:
1144
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
1145
+ pruned = {
1146
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1147
+ "containers_removed": (
1148
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
1149
+ or [c["Id"][7:19] for c in result.get("ContainersRemoved", [])]
1150
+ ),
1151
+ }
1152
+ self.log_action("prune_containers", params, pruned)
1153
+ return pruned
1154
+ except PodmanError as e:
1155
+ self.logger.error(f"PodmanError in prune_containers: {str(e)}")
1156
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1157
+ self.log_action("prune_containers", params, error=e)
1158
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1159
+ except Exception as e:
1160
+ self.logger.error(
1161
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
1162
+ )
1163
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1164
+ self.log_action("prune_containers", params, error=e)
1165
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1166
+
1167
+ def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
1168
+ params = {"force": force, "all": all}
1169
+ try:
1170
+ if all:
1171
+ volumes = self.client.volumes.list(all=True)
1172
+ removed = []
1173
+ for v in volumes:
1174
+ try:
1175
+ v.remove(force=force)
1176
+ removed.append(v.attrs["Name"])
1177
+ except Exception as e:
1178
+ self.logger.info(
1179
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
1180
+ )
1181
+ continue
1182
+ result = {
1183
+ "volumes_removed": removed,
1184
+ "space_reclaimed": "N/A (all volumes)",
1185
+ }
1186
+ else:
1187
+ result = self.client.volumes.prune()
1188
+ if result is None:
1189
+ result = {"SpaceReclaimed": 0, "VolumesRemoved": []}
1190
+ self.logger.debug(f"Raw prune_volumes result: {result}")
1191
+ pruned = {
1192
+ "space_reclaimed": self._format_size(
1193
+ result.get("SpaceReclaimed", 0)
1194
+ ),
1195
+ "volumes_removed": (
1196
+ [v["Name"] for v in result.get("VolumesRemoved", [])]
1197
+ or [v["Name"] for v in result.get("VolumesDeleted", [])]
1198
+ ),
1199
+ }
1200
+ result = pruned
1201
+ self.log_action("prune_volumes", params, result)
1202
+ return result
1203
+ except Exception as e:
1204
+ self.log_action("prune_volumes", params, error=e)
1205
+ raise RuntimeError(f"Failed to prune volumes: {str(e)}")
1206
+
1207
+ def prune_networks(self) -> Dict:
1208
+ params = {}
1209
+ try:
1210
+ result = self.client.networks.prune()
1211
+ if result is None:
1212
+ result = {"SpaceReclaimed": 0, "NetworksRemoved": []}
1213
+ self.logger.debug(f"Raw prune_networks result: {result}")
1214
+ pruned = {
1215
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1216
+ "networks_removed": (
1217
+ [n["Id"][7:19] for n in result.get("NetworksRemoved", [])]
1218
+ or [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
1219
+ ),
1220
+ }
1221
+ self.log_action("prune_networks", params, pruned)
1222
+ return pruned
1223
+ except Exception as e:
1224
+ self.log_action("prune_networks", params, error=e)
1225
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
1226
+
1227
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
1228
+ params = {"force": force, "all": all}
1229
+ try:
1230
+ cmd = (
1231
+ ["podman", "system", "prune", "--force"]
1232
+ if force
1233
+ else ["podman", "system", "prune"]
1234
+ )
1235
+ if all:
1236
+ cmd.append("--all")
1237
+ if all: # Include volumes if all=True
1238
+ cmd.append("--volumes")
1239
+ result = subprocess.run(cmd, capture_output=True, text=True)
1240
+ if result.returncode != 0:
1241
+ raise RuntimeError(result.stderr)
1242
+ self.logger.debug(f"Raw prune_system result: {result.stdout}")
1243
+ pruned = {
1244
+ "output": result.stdout.strip(),
1245
+ "space_reclaimed": "Check output",
1246
+ "images_removed": [], # Podman CLI doesn't provide detailed breakdown
1247
+ "containers_removed": [],
1248
+ "volumes_removed": [],
1249
+ "networks_removed": [],
1250
+ }
1251
+ self.log_action("prune_system", params, pruned)
1252
+ return pruned
1253
+ except Exception as e:
1254
+ self.log_action("prune_system", params, error=e)
1255
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
1256
+
1257
+ def get_version(self) -> Dict:
1258
+ params = {}
1259
+ try:
1260
+ version = self.client.version()
1261
+ result = {
1262
+ "version": version.get("Version", "unknown"),
1263
+ "api_version": version.get("APIVersion", "unknown"),
1264
+ "os": version.get("Os", "unknown"),
1265
+ "arch": version.get("Arch", "unknown"),
1266
+ "build_time": version.get("BuildTime", "unknown"),
1267
+ }
1268
+ self.log_action("get_version", params, result)
1269
+ return result
1270
+ except Exception as e:
1271
+ self.log_action("get_version", params, error=e)
1272
+ raise RuntimeError(f"Failed to get version: {str(e)}")
1273
+
1274
+ def get_info(self) -> Dict:
1275
+ params = {}
1276
+ try:
1277
+ info = self.client.info()
1278
+ host = info.get("host", {})
1279
+ result = {
1280
+ "containers_total": info.get("store", {}).get("containers", 0),
1281
+ "containers_running": host.get("runningContainers", 0),
1282
+ "images": info.get("store", {}).get("images", 0),
1283
+ "driver": host.get("graphDriverName", "unknown"),
1284
+ "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1285
+ "memory_total": self._format_size(host.get("memTotal", 0)),
1286
+ "swap_total": self._format_size(host.get("swapTotal", 0)),
1287
+ }
1288
+ self.log_action("get_info", params, result)
1289
+ return result
1290
+ except Exception as e:
1291
+ self.log_action("get_info", params, error=e)
1292
+ raise RuntimeError(f"Failed to get info: {str(e)}")
1293
+
1060
1294
  def list_images(self) -> List[Dict]:
1061
1295
  params = {}
1062
1296
  try:
@@ -1122,22 +1356,16 @@ class PodmanManager(ContainerManagerBase):
1122
1356
  self.log_action("pull_image", params, error=e)
1123
1357
  raise RuntimeError(f"Failed to pull image: {str(e)}")
1124
1358
 
1125
- def prune_images(self, force: bool = False, all: bool = False) -> Dict:
1126
- params = {"force": force, "all": all}
1359
+ def remove_image(self, image: str, force: bool = False) -> Dict:
1360
+ params = {"image": image, "force": force}
1127
1361
  try:
1128
- filters = {"dangling": True} if not all else {}
1129
- result = self.client.images.prune(filters=filters)
1130
- pruned = {
1131
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1132
- "images_removed": [
1133
- img["Id"][7:19] for img in result.get("ImagesRemoved", [])
1134
- ],
1135
- }
1136
- self.log_action("prune_images", params, pruned)
1137
- return pruned
1362
+ self.client.images.remove(image, force=force)
1363
+ result = {"removed": image}
1364
+ self.log_action("remove_image", params, result)
1365
+ return result
1138
1366
  except Exception as e:
1139
- self.log_action("prune_images", params, error=e)
1140
- raise RuntimeError(f"Failed to prune images: {str(e)}")
1367
+ self.log_action("remove_image", params, error=e)
1368
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
1141
1369
 
1142
1370
  def list_containers(self, all: bool = False) -> List[Dict]:
1143
1371
  params = {"all": all}
@@ -1225,131 +1453,6 @@ class PodmanManager(ContainerManagerBase):
1225
1453
  self.log_action("run_container", params, error=e)
1226
1454
  raise RuntimeError(f"Failed to run container: {str(e)}")
1227
1455
 
1228
- def prune_containers(self, force: bool = False) -> Dict:
1229
- params = {"force": force}
1230
- try:
1231
- result = self.client.containers.prune()
1232
- pruned = {
1233
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1234
- "containers_removed": [
1235
- c["Id"][7:19] for c in result.get("ContainersRemoved", [])
1236
- ],
1237
- }
1238
- self.log_action("prune_containers", params, pruned)
1239
- return pruned
1240
- except Exception as e:
1241
- self.log_action("prune_containers", params, error=e)
1242
- raise RuntimeError(f"Failed to prune containers: {str(e)}")
1243
-
1244
- def list_networks(self) -> List[Dict]:
1245
- params = {}
1246
- try:
1247
- networks = self.client.networks.list()
1248
- result = []
1249
- for net in networks:
1250
- attrs = net.attrs
1251
- containers = len(attrs.get("Containers", {}))
1252
- created = attrs.get("Created", None)
1253
- created_str = self._parse_timestamp(created)
1254
- simplified = {
1255
- "id": attrs.get("Id", "unknown")[7:19],
1256
- "name": attrs.get("Name", "unknown"),
1257
- "driver": attrs.get("Driver", "unknown"),
1258
- "scope": attrs.get("Scope", "unknown"),
1259
- "containers": containers,
1260
- "created": created_str,
1261
- }
1262
- result.append(simplified)
1263
- self.log_action("list_networks", params, result)
1264
- return result
1265
- except Exception as e:
1266
- self.log_action("list_networks", params, error=e)
1267
- raise RuntimeError(f"Failed to list networks: {str(e)}")
1268
-
1269
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
1270
- params = {"name": name, "driver": driver}
1271
- try:
1272
- network = self.client.networks.create(name, driver=driver)
1273
- attrs = network.attrs
1274
- created = attrs.get("Created", None)
1275
- created_str = self._parse_timestamp(created)
1276
- result = {
1277
- "id": attrs.get("Id", "unknown")[7:19],
1278
- "name": attrs.get("Name", name),
1279
- "driver": attrs.get("Driver", driver),
1280
- "scope": attrs.get("Scope", "unknown"),
1281
- "created": created_str,
1282
- }
1283
- self.log_action("create_network", params, result)
1284
- return result
1285
- except Exception as e:
1286
- self.log_action("create_network", params, error=e)
1287
- raise RuntimeError(f"Failed to create network: {str(e)}")
1288
-
1289
- def prune_networks(self) -> Dict:
1290
- params = {}
1291
- try:
1292
- result = self.client.networks.prune()
1293
- pruned = {
1294
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1295
- "networks_removed": [
1296
- n["Id"][7:19] for n in result.get("NetworksRemoved", [])
1297
- ],
1298
- }
1299
- self.log_action("prune_networks", params, pruned)
1300
- return pruned
1301
- except Exception as e:
1302
- self.log_action("prune_networks", params, error=e)
1303
- raise RuntimeError(f"Failed to prune networks: {str(e)}")
1304
-
1305
- def get_version(self) -> Dict:
1306
- params = {}
1307
- try:
1308
- version = self.client.version()
1309
- result = {
1310
- "version": version.get("Version", "unknown"),
1311
- "api_version": version.get("APIVersion", "unknown"),
1312
- "os": version.get("Os", "unknown"),
1313
- "arch": version.get("Arch", "unknown"),
1314
- "build_time": version.get("BuildTime", "unknown"),
1315
- }
1316
- self.log_action("get_version", params, result)
1317
- return result
1318
- except Exception as e:
1319
- self.log_action("get_version", params, error=e)
1320
- raise RuntimeError(f"Failed to get version: {str(e)}")
1321
-
1322
- def get_info(self) -> Dict:
1323
- params = {}
1324
- try:
1325
- info = self.client.info()
1326
- host = info.get("host", {})
1327
- result = {
1328
- "containers_total": info.get("store", {}).get("containers", 0),
1329
- "containers_running": host.get("runningContainers", 0),
1330
- "images": info.get("store", {}).get("images", 0),
1331
- "driver": host.get("graphDriverName", "unknown"),
1332
- "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1333
- "memory_total": self._format_size(host.get("memTotal", 0)),
1334
- "swap_total": self._format_size(host.get("swapTotal", 0)),
1335
- }
1336
- self.log_action("get_info", params, result)
1337
- return result
1338
- except Exception as e:
1339
- self.log_action("get_info", params, error=e)
1340
- raise RuntimeError(f"Failed to get info: {str(e)}")
1341
-
1342
- def remove_image(self, image: str, force: bool = False) -> Dict:
1343
- params = {"image": image, "force": force}
1344
- try:
1345
- self.client.images.remove(image, force=force)
1346
- result = {"removed": image}
1347
- self.log_action("remove_image", params, result)
1348
- return result
1349
- except Exception as e:
1350
- self.log_action("remove_image", params, error=e)
1351
- raise RuntimeError(f"Failed to remove image: {str(e)}")
1352
-
1353
1456
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
1354
1457
  params = {"container_id": container_id, "timeout": timeout}
1355
1458
  try:
@@ -1455,40 +1558,50 @@ class PodmanManager(ContainerManagerBase):
1455
1558
  self.log_action("remove_volume", params, error=e)
1456
1559
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
1457
1560
 
1458
- def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
1459
- params = {"force": force, "all": all}
1561
+ def list_networks(self) -> List[Dict]:
1562
+ params = {}
1460
1563
  try:
1461
- if all:
1462
- # Remove all volumes
1463
- volumes = self.client.volumes.list(all=True)
1464
- removed = []
1465
- for v in volumes:
1466
- try:
1467
- v.remove(force=force)
1468
- removed.append(v.attrs["Name"])
1469
- except Exception as e:
1470
- logging.info(f"Info: {e}")
1471
- pass
1472
- result = {
1473
- "volumes_removed": removed,
1474
- "space_reclaimed": "N/A (all volumes)",
1475
- }
1476
- else:
1477
- result = self.client.volumes.prune()
1478
- pruned = {
1479
- "space_reclaimed": self._format_size(
1480
- result.get("SpaceReclaimed", 0)
1481
- ),
1482
- "volumes_removed": [
1483
- v["Name"] for v in result.get("VolumesRemoved", [])
1484
- ],
1564
+ networks = self.client.networks.list()
1565
+ result = []
1566
+ for net in networks:
1567
+ attrs = net.attrs
1568
+ containers = len(attrs.get("Containers", {}))
1569
+ created = attrs.get("Created", None)
1570
+ created_str = self._parse_timestamp(created)
1571
+ simplified = {
1572
+ "id": attrs.get("Id", "unknown")[7:19],
1573
+ "name": attrs.get("Name", "unknown"),
1574
+ "driver": attrs.get("Driver", "unknown"),
1575
+ "scope": attrs.get("Scope", "unknown"),
1576
+ "containers": containers,
1577
+ "created": created_str,
1485
1578
  }
1486
- result = pruned
1487
- self.log_action("prune_volumes", params, result)
1579
+ result.append(simplified)
1580
+ self.log_action("list_networks", params, result)
1488
1581
  return result
1489
1582
  except Exception as e:
1490
- self.log_action("prune_volumes", params, error=e)
1491
- raise RuntimeError(f"Failed to prune volumes: {str(e)}")
1583
+ self.log_action("list_networks", params, error=e)
1584
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
1585
+
1586
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
1587
+ params = {"name": name, "driver": driver}
1588
+ try:
1589
+ network = self.client.networks.create(name, driver=driver)
1590
+ attrs = network.attrs
1591
+ created = attrs.get("Created", None)
1592
+ created_str = self._parse_timestamp(created)
1593
+ result = {
1594
+ "id": attrs.get("Id", "unknown")[7:19],
1595
+ "name": attrs.get("Name", name),
1596
+ "driver": attrs.get("Driver", driver),
1597
+ "scope": attrs.get("Scope", "unknown"),
1598
+ "created": created_str,
1599
+ }
1600
+ self.log_action("create_network", params, result)
1601
+ return result
1602
+ except Exception as e:
1603
+ self.log_action("create_network", params, error=e)
1604
+ raise RuntimeError(f"Failed to create network: {str(e)}")
1492
1605
 
1493
1606
  def remove_network(self, network_id: str) -> Dict:
1494
1607
  params = {"network_id": network_id}
@@ -1502,26 +1615,6 @@ class PodmanManager(ContainerManagerBase):
1502
1615
  self.log_action("remove_network", params, error=e)
1503
1616
  raise RuntimeError(f"Failed to remove network: {str(e)}")
1504
1617
 
1505
- def prune_system(self, force: bool = False, all: bool = False) -> Dict:
1506
- params = {"force": force, "all": all}
1507
- try:
1508
- # Podman system prune uses CLI, as podman-py may not have direct support
1509
- cmd = ["podman", "system", "prune"]
1510
- if all:
1511
- cmd.append("--all")
1512
- result = subprocess.run(cmd, capture_output=True, text=True)
1513
- if result.returncode != 0:
1514
- raise RuntimeError(result.stderr)
1515
- pruned = {
1516
- "output": result.stdout.strip(),
1517
- "space_reclaimed": "Check output",
1518
- }
1519
- self.log_action("prune_system", params, pruned)
1520
- return pruned
1521
- except Exception as e:
1522
- self.log_action("prune_system", params, error=e)
1523
- raise RuntimeError(f"Failed to prune system: {str(e)}")
1524
-
1525
1618
  def compose_up(
1526
1619
  self, compose_file: str, detach: bool = True, build: bool = False
1527
1620
  ) -> str:
@@ -1608,21 +1701,24 @@ class PodmanManager(ContainerManagerBase):
1608
1701
  raise NotImplementedError("Swarm not supported in Podman")
1609
1702
 
1610
1703
 
1704
+ def is_app_installed(app_name: str = "docker") -> bool:
1705
+ return shutil.which(app_name.lower()) is not None
1706
+
1707
+
1611
1708
  def create_manager(
1612
1709
  manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
1613
1710
  ) -> ContainerManagerBase:
1614
1711
  if manager_type is None:
1615
- manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
1712
+ manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
1616
1713
  if manager_type is None:
1617
- # Autodetect
1618
- if PodmanClient is not None:
1714
+ if is_app_installed("podman"):
1619
1715
  try:
1620
1716
  test_client = PodmanClient()
1621
1717
  test_client.close()
1622
1718
  manager_type = "podman"
1623
1719
  except Exception:
1624
1720
  pass
1625
- if manager_type is None and docker is not None:
1721
+ if is_app_installed("docker"):
1626
1722
  try:
1627
1723
  test_client = docker.from_env()
1628
1724
  test_client.close()
@@ -1721,7 +1817,7 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
1721
1817
  )
1722
1818
 
1723
1819
 
1724
- def container_manager(argv):
1820
+ def container_manager():
1725
1821
  parser = argparse.ArgumentParser(
1726
1822
  description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
1727
1823
  )
@@ -1826,7 +1922,7 @@ def container_manager(argv):
1826
1922
  parser.add_argument("--force", action="store_true", help="Force removal")
1827
1923
  parser.add_argument("-h", "--help", action="store_true", help="Show help")
1828
1924
 
1829
- args = parser.parse_args(argv)
1925
+ args = parser.parse_args()
1830
1926
 
1831
1927
  if args.help:
1832
1928
  usage()
@@ -1980,7 +2076,7 @@ def container_manager(argv):
1980
2076
  )
1981
2077
 
1982
2078
  if prune_containers:
1983
- print(json.dumps(manager.prune_containers(force), indent=2))
2079
+ print(json.dumps(manager.prune_containers(), indent=2))
1984
2080
 
1985
2081
  if get_container_logs:
1986
2082
  if not container_logs_id:
@@ -2028,7 +2124,7 @@ def container_manager(argv):
2028
2124
  print(json.dumps(manager.remove_network(remove_network_id), indent=2))
2029
2125
 
2030
2126
  if prune_networks:
2031
- print(json.dumps(manager.prune_networks(force), indent=2))
2127
+ print(json.dumps(manager.prune_networks(), indent=2))
2032
2128
 
2033
2129
  if prune_system:
2034
2130
  print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
@@ -2100,4 +2196,4 @@ if __name__ == "__main__":
2100
2196
  if len(sys.argv) < 2:
2101
2197
  usage()
2102
2198
  sys.exit(2)
2103
- container_manager(sys.argv[1:])
2199
+ container_manager()
@@ -39,6 +39,29 @@ def to_boolean(string):
39
39
  raise ValueError(f"Cannot convert '{string}' to boolean")
40
40
 
41
41
 
42
+ def parse_image_string(image: str, default_tag: str = "latest") -> tuple[str, str]:
43
+ """
44
+ Parse a container image string into image and tag components.
45
+
46
+ Args:
47
+ image: Input image string (e.g., 'registry.arpa/ubuntu/ubuntu:latest' or 'nginx')
48
+ default_tag: Fallback tag if none is specified (default: 'latest')
49
+
50
+ Returns:
51
+ Tuple of (image, tag) where image includes registry/repository, tag is the tag or default_tag
52
+ """
53
+ # Split on the last ':' to separate image and tag
54
+ if ":" in image:
55
+ parts = image.rsplit(":", 1)
56
+ image_name, tag = parts[0], parts[1]
57
+ # Ensure tag is valid (not a port or malformed)
58
+ if "/" in tag or not tag:
59
+ # If tag contains '/' or is empty, assume no tag was provided
60
+ return image, default_tag
61
+ return image_name, tag
62
+ return image, default_tag
63
+
64
+
42
65
  environment_silent = os.environ.get("SILENT", False)
43
66
  environment_log_file = os.environ.get("LOG_FILE", None)
44
67
  environment_container_manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
@@ -171,8 +194,13 @@ async def list_images(
171
194
  tags={"container_management"},
172
195
  )
173
196
  async def pull_image(
174
- image: str = Field(description="Image name to pull"),
175
- tag: str = Field(description="Image tag", default="latest"),
197
+ image: str = Field(
198
+ description="Image name to pull (e.g., nginx, registry.arpa/ubuntu/ubuntu:latest)."
199
+ ),
200
+ tag: str = Field(
201
+ description="Image tag (overridden if tag is included in image string)",
202
+ default="latest",
203
+ ),
176
204
  platform: Optional[str] = Field(
177
205
  description="Platform (e.g., linux/amd64)", default=None
178
206
  ),
@@ -191,12 +219,14 @@ async def pull_image(
191
219
  ),
192
220
  ) -> Dict:
193
221
  logger = logging.getLogger("ContainerManager")
222
+ # Parse image string to separate image and tag
223
+ parsed_image, parsed_tag = parse_image_string(image, tag)
194
224
  logger.debug(
195
- f"Pulling image {image}:{tag} for {manager_type}, silent: {silent}, log_file: {log_file}"
225
+ f"Pulling image {parsed_image}:{parsed_tag} for {manager_type}, silent: {silent}, log_file: {log_file}"
196
226
  )
197
227
  try:
198
228
  manager = create_manager(manager_type, silent, log_file)
199
- return manager.pull_image(image, tag, platform)
229
+ return manager.pull_image(parsed_image, parsed_tag, platform)
200
230
  except Exception as e:
201
231
  logger.error(f"Failed to pull image: {str(e)}")
202
232
  raise RuntimeError(f"Failed to pull image: {str(e)}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: container-manager-mcp
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Container Manager manage Docker, Docker Swarm, and Podman containers as an MCP Server
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -48,7 +48,7 @@ Dynamic: license-file
48
48
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/container-manager-mcp)
49
49
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/container-manager-mcp)
50
50
 
51
- *Version: 1.0.3*
51
+ *Version: 1.0.4*
52
52
 
53
53
  Container Manager MCP Server provides a robust interface to manage Docker and Podman containers, networks, volumes, and Docker Swarm services through a FastMCP server, enabling programmatic and remote container management.
54
54
 
@@ -0,0 +1,10 @@
1
+ container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
2
+ container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
3
+ container_manager_mcp/container_manager.py,sha256=sHxM17dj7luzp1Ak2aQJ3JLqHd3POO3S2_W32HQwWog,89167
4
+ container_manager_mcp/container_manager_mcp.py,sha256=E3FOTFILyYYoJF3Erkpl5zapkiDi2pKlJlkZByc-8QE,47361
5
+ container_manager_mcp-1.0.4.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
6
+ container_manager_mcp-1.0.4.dist-info/METADATA,sha256=XhTi2pEQeO1cfnrVgVViq_LiwGqLqUyhsogD-jXUFys,8238
7
+ container_manager_mcp-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ container_manager_mcp-1.0.4.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
9
+ container_manager_mcp-1.0.4.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
10
+ container_manager_mcp-1.0.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
2
- container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
3
- container_manager_mcp/container_manager.py,sha256=1HN_sKQFGVtf35wk66Uez9MtHOv2B9LJ4tbTbEzCXEA,84152
4
- container_manager_mcp/container_manager_mcp.py,sha256=M43YmLt_qoOfpnlK2VyyGX2vJ6gMBxepSqBauvMS4AA,46204
5
- container_manager_mcp-1.0.3.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
6
- container_manager_mcp-1.0.3.dist-info/METADATA,sha256=RfIVbh9AeL0roB2yyqtXBJR2o8H7MqTpV82epVm0V1I,8238
7
- container_manager_mcp-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- container_manager_mcp-1.0.3.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
9
- container_manager_mcp-1.0.3.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
10
- container_manager_mcp-1.0.3.dist-info/RECORD,,