diff --git a/.github/prompts/docker-manager-python.prompt.md b/.github/prompts/docker-manager-python.prompt.md new file mode 100644 index 0000000..1627e39 --- /dev/null +++ b/.github/prompts/docker-manager-python.prompt.md @@ -0,0 +1,9 @@ +Create a Python command-line application in the `~/shell/docker-manager/` directory to manage Docker containers defined in subdirectories of `~/docker/`. Each subdirectory contains either a `docker-compose.yml` or `compose.yml` file. The application must: + +- List the names of currently running containers defined in `~/docker/`. +- Provide commands to stop, update (pull latest images), and restart these containers. +- Be executable from any directory via the command line, without requiring navigation to the script's location. +- Output results and status messages to the console. +- Include setup instructions for installation and usage. + +Provide complete Python code and a `README.md` with setup and usage instructions. Use only files within `~/shell/docker-manager/` to avoid cluttering other directories. Reference the [Docker Compose CLI documentation](https://docs.docker.com/compose/reference/) for command usage. \ No newline at end of file diff --git a/docker-manager/README.md b/docker-manager/README.md new file mode 100644 index 0000000..58f8adb --- /dev/null +++ b/docker-manager/README.md @@ -0,0 +1,95 @@ +# Docker Manager + +A Python command-line application to manage Docker containers defined in subdirectories of `~/docker/`. + +## Features + +- **List**: View currently running containers across all your projects. +- **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. + +## Prerequisites + +- Python 3 +- Docker and Docker Compose (plugin) installed. +- A `~/docker/` directory containing subdirectories for each of your projects. +- Each project subdirectory must contain a `docker-compose.yml` or `compose.yml` file. + +## Installation + +1. **Ensure the script is executable:** + ```bash + chmod +x ~/shell/docker-manager/docker-manager.py + ``` + +2. **Make it accessible from anywhere:** + You can create a symbolic link to a directory in your `$PATH` (e.g., `~/.local/bin` or `/usr/local/bin`), or add an alias. + + **Option A: Symbolic Link (Recommended)** + ```bash + mkdir -p ~/.local/bin + ln -s ~/shell/docker-manager/docker-manager.py ~/.local/bin/dm + ``` + *Note: Ensure `~/.local/bin` is in your `$PATH`.* + + **Option B: Alias** + Add the following to your `~/.zshrc` or `~/.bashrc`: + ```bash + alias dm='~/shell/docker-manager/docker-manager.py' + ``` + +## Usage + +Run the application using the command name you set up (e.g., `dm`). + +### List Running Containers +```bash +dm list +``` + +### Stop Containers +Stop a specific project: +```bash +dm stop project_name +``` +Stop all projects: +```bash +dm stop --all +``` + +### Update Containers +Pull latest images and recreate containers for a specific project: +```bash +dm update project_name +``` +Update all projects: +```bash +dm update --all +``` + +### Restart Containers +Restart a specific project: +```bash +dm restart project_name +``` +Restart all projects: +```bash +dm restart --all +``` + +## Directory Structure Example + +The tool expects a structure like this: + +``` +~/docker/ +├── media-server/ +│ └── docker-compose.yml +├── web-app/ +│ └── compose.yml +└── database/ + └── docker-compose.yml +``` + +In this example, the project names are `media-server`, `web-app`, and `database`. diff --git a/docker-manager/docker-manager.py b/docker-manager/docker-manager.py new file mode 100755 index 0000000..d343f98 --- /dev/null +++ b/docker-manager/docker-manager.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +""" +Docker Manager Application + +This script manages Docker containers defined in subdirectories of `~/docker/`. +Each subdirectory is treated as a project and must contain a `docker-compose.yml` or `compose.yml`. + +Usage Examples: + 1. List all running containers: + $ dm list + + 2. Update a specific project (pulls latest images and recreates containers): + $ dm update media-server + + 3. Stop all projects (with confirmation prompt): + $ dm stop + + 4. Restart all projects without confirmation: + $ dm restart --all + + 5. Stop a specific project: + $ dm stop web-app + +Purpose: + - Simplifies management of multiple Docker Compose projects. + - Provides a unified interface for common operations (list, stop, update, restart). + - Eliminates the need to navigate to specific directories to run docker commands. + +To make the command available as dm from any directory, run: + chmod +x ~/shell/docker-manager/docker-manager.py + mkdir -p ~/.local/bin + ln -s ~/shell/docker-manager/docker-manager.py ~/.local/bin/dm +""" + +import os +import argparse +import subprocess +from pathlib import Path + +# Configuration +DOCKER_ROOT = Path(os.path.expanduser("~/docker")) + + +def get_projects(): + """Scan DOCKER_ROOT for subdirectories with compose files.""" + projects = {} + if not DOCKER_ROOT.exists(): + print(f"Error: Directory {DOCKER_ROOT} does not exist.") + return projects + + for item in DOCKER_ROOT.iterdir(): + if item.is_dir(): + if (item / "docker-compose.yml").exists() or (item / "compose.yml").exists(): + projects[item.name] = item + return projects + + +def run_command(cmd, cwd, capture_output=False): + """Run a shell command in a specific directory.""" + try: + result = subprocess.run( + cmd, + cwd=cwd, + check=True, + text=True, + capture_output=capture_output + ) + return result + except subprocess.CalledProcessError as e: + if capture_output: + return e + print(f"Error running command: {' '.join(cmd)}") + print(f"cwd: {cwd}") + return None + + +def list_containers(projects): + """List running containers for all projects.""" + print(f"{'Project':<20} | {'Container Name':<40} | {'State':<10}") + print("-" * 75) + + found_any = False + for name, path in sorted(projects.items()): + # Get running container names + cmd = ["docker", "compose", "ps", "--format", + "{{.Names}}", "--filter", "status=running"] + res = run_command(cmd, path, capture_output=True) + + if res and res.returncode == 0: + containers = res.stdout.strip().split('\n') + containers = [c for c in containers if c] # filter empty + if containers: + found_any = True + for container in containers: + print(f"{name:<20} | {container:<40} | Running") + + if not found_any: + print("No running containers found in managed projects.") + + +def is_project_running(path): + """Check if any containers in the project are running.""" + cmd = ["docker", "compose", "ps", "--format", + "json", "--filter", "status=running"] + res = run_command(cmd, path, capture_output=True) + if res and res.returncode == 0: + # If output is not empty/just brackets, something is running + output = res.stdout.strip() + return output and output != "[]" + return False + + +def manage_project(projects, action, target, extra_args=None): + """ + Execute the specified action (stop, update, restart, logs) on target project(s). + + Args: + projects (dict): Dictionary of project names to their paths. + action (str): The action to perform ('stop', 'update', 'restart', 'logs'). + target (str): The target project name or 'all'. + extra_args (list): Additional arguments for the command (e.g. for logs). + """ + + targets = [] + # Determine which projects to target + if target == "all": + targets = sorted(projects.items()) + elif target in projects: + targets = [(target, projects[target])] + else: + print(f"Error: Project '{target}' not found in {DOCKER_ROOT}") + print("Available projects:", ", ".join(sorted(projects.keys()))) + return + + # Logs are special, usually run on one project interactively + if action == "logs": + if len(targets) > 1: + print("Error: Logs can only be viewed for one project at a time.") + return + name, path = targets[0] + print(f"Viewing logs for {name}...") + cmd = ["docker", "compose", "logs"] + (extra_args or []) + # For logs, we want to stream output directly to stdout, so no capture_output + # and we might want to allow Ctrl+C to exit gracefully + try: + subprocess.run(cmd, cwd=path, check=False) + except KeyboardInterrupt: + print("\nLog view stopped.") + return + + print(f"Performing '{action}' on {len(targets)} project(s)...") + + for name, path in targets: + print(f"\n[{name}] -> {action}...") + + # Execute the requested action + if action == "stop": + # Stop containers without removing them + run_command(["docker", "compose", "stop"], path) + elif action == "restart": + # Restart containers + run_command(["docker", "compose", "restart"], path) + elif action == "update": + # Update process: Check running -> Stop -> Pull -> Start if was running + was_running = is_project_running(path) + + if was_running: + print(f" Stopping running containers for {name}...") + run_command(["docker", "compose", "stop"], path) + + print(f" Pulling latest images for {name}...") + pull_res = run_command(["docker", "compose", "pull"], path) + + if pull_res and pull_res.returncode == 0: + if was_running: + print(f" Restarting containers for {name}...") + run_command(["docker", "compose", "up", "-d"], path) + else: + print( + f" Project {name} was not running. Images updated, but not started.") + else: + print(f" Failed to pull images for {name}. Skipping update.") + + +def main(): + """Main entry point for the application.""" + parser = argparse.ArgumentParser( + description="Manage Docker containers in ~/docker/") + subparsers = parser.add_subparsers( + dest="command", help="Command to execute") + + # List command configuration + subparsers.add_parser("list", help="List running containers") + + # Stop command configuration + stop_parser = subparsers.add_parser("stop", help="Stop containers") + stop_parser.add_argument("project", nargs="?", default=None, + help="Project name (optional). If omitted, asks for confirmation to stop ALL.") + stop_parser.add_argument("--all", action="store_true", + help="Stop all projects without confirmation prompt if specified") + + # Update command configuration + update_parser = subparsers.add_parser( + "update", help="Pull and update containers") + update_parser.add_argument("project", nargs="?", default=None, + help="Project name (optional). If omitted, asks for confirmation to update ALL.") + update_parser.add_argument("--all", action="store_true", + help="Update all projects without confirmation prompt if specified") + + # Restart command configuration + restart_parser = subparsers.add_parser( + "restart", help="Restart containers") + restart_parser.add_argument("project", nargs="?", default=None, + help="Project name (optional). If omitted, asks for confirmation to restart ALL.") + restart_parser.add_argument("--all", action="store_true", + help="Restart all projects without confirmation prompt if specified") + + # Logs command configuration + logs_parser = subparsers.add_parser("logs", help="View container logs") + logs_parser.add_argument("project", help="Project name to view logs for") + logs_parser.add_argument( + "-f", "--follow", action="store_true", help="Follow log output") + logs_parser.add_argument( + "--tail", default="all", help="Number of lines to show from the end of the logs (default: all)") + + args = parser.parse_args() + + # Scan for projects + projects = get_projects() + if not projects: + print(f"No docker projects found in {DOCKER_ROOT}") + return + + # Dispatch commands + if args.command == "list": + list_containers(projects) + elif args.command == "logs": + # Prepare extra args for logs + extra_args = [] + if args.follow: + extra_args.append("-f") + if args.tail: + extra_args.extend(["--tail", args.tail]) + + manage_project(projects, "logs", args.project, extra_args=extra_args) + + elif args.command in ["stop", "update", "restart"]: + target = args.project + + # Handle "all" logic safely with user confirmation + if target is None: + if args.all: + target = "all" + else: + # Interactive check if user wants to apply to all + print( + f"No project specified. Do you want to {args.command} ALL projects? (y/N)") + choice = input().lower() + if choice == 'y': + target = "all" + else: + print("Operation cancelled. Specify a project name or use --all.") + return + + manage_project(projects, args.command, target) + else: + # Show help if no command provided + parser.print_help() + + +if __name__ == "__main__": + main()