From e850221326a1000e8a53bd26bc55e9c3acf18eec Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Mon, 8 Dec 2025 10:54:29 +0000 Subject: [PATCH] feat: Add describe and volumes commands to Docker Manager for detailed project insights --- docker-manager/README.md | 2 + docker-manager/docker-manager.py | 149 +++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/docker-manager/README.md b/docker-manager/README.md index 041abe6..be7d3ec 100644 --- a/docker-manager/README.md +++ b/docker-manager/README.md @@ -5,6 +5,8 @@ A Python command-line application to manage Docker containers defined in subdire ## Features - **List**: View currently running containers across all your projects. +- **Describe**: Show detailed information about containers in a specific project, including descriptions. +- **Volumes**: List volumes used by a specific project. - **Stop**: Stop containers for a specific project or all projects. - **Update**: Pull the latest images and recreate containers (equivalent to `docker compose pull && docker compose up -d`). - **Restart**: Restart containers for a specific project or all projects. diff --git a/docker-manager/docker-manager.py b/docker-manager/docker-manager.py index da48cec..874a3c2 100755 --- a/docker-manager/docker-manager.py +++ b/docker-manager/docker-manager.py @@ -35,6 +35,7 @@ To make the command available as dm from any directory, run: import os import argparse import subprocess +import json from pathlib import Path # Configuration @@ -113,6 +114,142 @@ def is_project_running(path): return False +def describe_project(projects, target): + """Show detailed information about containers in a project.""" + if target not in projects: + print(f"Error: Project '{target}' not found.") + return + + path = projects[target] + print(f"Describing project: {target}") + + cmd = ["docker", "compose", "ps", "--format", "json"] + res = run_command(cmd, path, capture_output=True) + + if res and res.returncode == 0: + lines = res.stdout.strip().split('\n') + found_any = False + for line in lines: + if not line: + continue + try: + container = json.loads(line) + found_any = True + name = container.get('Name', 'N/A') + print("-" * 60) + print(f"Service: {container.get('Service', 'N/A')}") + print(f"Container: {name}") + print(f"Image: {container.get('Image', 'N/A')}") + print(f"State: {container.get('State', 'N/A')}") + print(f"Status: {container.get('Status', 'N/A')}") + + ports = container.get('Ports', '') + if ports: + print(f"Ports: {ports}") + + # Get details via inspect for better parsing (Labels and Mounts) + inspect_cmd = ["docker", "inspect", name, "--format", "{{json .}}"] + inspect_res = run_command(inspect_cmd, path, capture_output=True) + if inspect_res and inspect_res.returncode == 0: + try: + details = json.loads(inspect_res.stdout.strip()) + + # Description from labels + labels = details.get("Config", {}).get("Labels", {}) + description = labels.get("org.opencontainers.image.description") + if description: + print(f"Description: {description}") + + # Volumes/Mounts + mounts = details.get("Mounts", []) + if mounts: + print("Volumes:") + print(f" {'Name/Type':<20} | {'Source (Local Path)':<50} | {'Destination'}") + print(f" {'-'*20} | {'-'*50} | {'-'*20}") + for mount in mounts: + m_type = mount.get("Type") + vol_name = mount.get("Name", "") + source = mount.get("Source", "") + dest = mount.get("Destination", "") + + # If it's a bind mount, use "Bind" as name, otherwise use volume name + display_name = vol_name if vol_name else f"[{m_type}]" + + # Truncate source if too long for cleaner display, or just let it wrap? + # Let's just print it. + print(f" {display_name:<20} | {source:<50} | {dest}") + + except json.JSONDecodeError: + pass + + except json.JSONDecodeError: + pass + + if not found_any: + print("No containers found for this project.") + else: + print("Failed to get container info.") + + +def list_project_volumes(projects, target): + """List volumes used by containers in a project.""" + if target not in projects: + print(f"Error: Project '{target}' not found.") + return + + path = projects[target] + print(f"Volumes for project: {target}") + print("-" * 100) + print(f"{'Service':<20} | {'Type':<10} | {'Name/Source':<40} | {'Destination'}") + print("-" * 100) + + cmd = ["docker", "compose", "ps", "--format", "json"] + res = run_command(cmd, path, capture_output=True) + + if res and res.returncode == 0: + lines = res.stdout.strip().split('\n') + found_any = False + for line in lines: + if not line: + continue + try: + container = json.loads(line) + name = container.get('Name', 'N/A') + service = container.get('Service', 'N/A') + + # Get details via inspect + inspect_cmd = ["docker", "inspect", name, "--format", "{{json .Mounts}}"] + inspect_res = run_command(inspect_cmd, path, capture_output=True) + + if inspect_res and inspect_res.returncode == 0: + try: + mounts = json.loads(inspect_res.stdout.strip()) + if mounts: + found_any = True + for mount in mounts: + m_type = mount.get("Type", "unknown") + source = mount.get("Source", "") + dest = mount.get("Destination", "") + name_or_source = mount.get("Name", "") + + if m_type == "bind": + name_or_source = source + elif not name_or_source: + name_or_source = source + + print(f"{service:<20} | {m_type:<10} | {name_or_source:<40} | {dest}") + except json.JSONDecodeError: + pass + + except json.JSONDecodeError: + pass + + if not found_any: + print("No volumes found for this project.") + else: + print("Failed to get container info.") + + def manage_project(projects, action, target, extra_args=None): """ Execute the specified action (stop, update, restart, logs) on target project(s). @@ -195,6 +332,14 @@ def main(): # List command configuration subparsers.add_parser("list", help="List running containers") + # Describe command configuration + describe_parser = subparsers.add_parser("describe", help="Show details of a project's containers") + describe_parser.add_argument("project", help="Project name to describe") + + # Volumes command configuration + volumes_parser = subparsers.add_parser("volumes", help="List volumes used by a project") + volumes_parser.add_argument("project", help="Project name to list volumes for") + # Stop command configuration stop_parser = subparsers.add_parser("stop", help="Stop containers") stop_parser.add_argument("project", nargs="?", default=None, @@ -237,6 +382,10 @@ def main(): # Dispatch commands if args.command == "list": list_containers(projects) + elif args.command == "describe": + describe_project(projects, args.project) + elif args.command == "volumes": + list_project_volumes(projects, args.project) elif args.command == "logs": # Prepare extra args for logs extra_args = []