diff --git a/README.md b/README.md index 62248cc..1ae303b 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,67 @@ -# deu (Docker Environment Utility) +# DEU - Docker Environment Utility -A simple utility to create and manage development containers. +A simple utility to create and manage development containers using Docker Compose. ## Overview -deu helps you quickly set up development containers with a consistent configuration. It creates a Docker Compose setup and manages container lifecycle. +`deu` helps you quickly set up development environments in Docker containers. It creates a Docker Compose configuration and manages container states (start, stop, logs, etc.). ## Installation ```bash -# Make the script executable chmod +x deu.py - -# Optionally, create a symlink -ln -s $(pwd)/deu.py /usr/local/bin/deu - -# Or copy it - -cp $(pwd)/deu.py /usr/local/bin/deu +sudo ln -s $(pwd)/deu.py /usr/local/bin/deu ``` ## Usage -### Initialize a container +### Initialize a Container + +Create a new development container: ```bash -# Initialize with a random service name -deu init .container --image python:3.11 +# Local container (in current directory) +deu init --image ubuntu:24.04 -# Initialize with a specific service name -deu init .container --image python:3.11 --service my_service +# Global container (stored in ~/.config/deu/) +deu init -g --image ubuntu:24.04 + +# With custom service name +deu init --image ubuntu:24.04 --service my_container ``` -### Container Management +### Manage Containers + +All commands support both local and global containers: ```bash -# Start container in background -deu background - # Activate container shell -deu activate +deu activate # Use local container +deu activate my_container # Use specific container (local or global) + +# Start container in background +deu background # Use local container +deu background my_container # View container logs -deu logs +deu logs # Use local container +deu logs my_container # Stop container -deu stop +deu stop # Use local container +deu stop my_container # Remove container -deu rm +deu rm # Use local container +deu rm my_container ``` ## Configuration -deu creates two configuration files: +`deu` creates two configuration files: -1. `.container/docker-compose.yml`: Docker Compose configuration -2. `deu.toml`: Container metadata and settings +1. `deu.toml` - Container configuration (local or in ~/.config/deu/) +2. `.container/docker-compose.yml` - Docker Compose configuration (local or in ~/.config/deu/.container_name/) ## Requirements diff --git a/deu.py b/deu.py index 6fea048..4a96332 100755 --- a/deu.py +++ b/deu.py @@ -199,60 +199,66 @@ def get_container_config() -> ContainerConfig: sys.exit(1) -def handle_activate() -> None: - """Activate the container shell.""" - config = get_container_config() - print(f"Trying to activate container shell for service '{config.service}'...") - - # Check if container is running, start it if not - try: - ps_output = subprocess.run( - ["docker", "compose", "-f", str(config.path / "docker-compose.yml"), "ps"], - check=True, - capture_output=True, - text=True, - ) - if config.service not in ps_output.stdout: - logger.info(f"Container '{config.service}' is not running. Starting it...") - run_docker_compose_command(config.path, "up -d") - except subprocess.CalledProcessError: - logger.info(f"Container '{config.service}' is not running. Starting it...") - run_docker_compose_command(config.path, "up -d") - - # Use appropriate shell based on configuration - shell = "bash" if config.shell == ShellType.BASH else "sh" - run_docker_compose_command(config.path, f"exec {config.service} {shell}") +def get_global_config_dir() -> Path: + """Get the global configuration directory.""" + return Path.home() / ".config" / "deu" -def handle_background() -> None: - """Start the container in background mode.""" - container_path = get_container_config().path - run_docker_compose_command(container_path, "up", detach=True) +def find_container_config( + container_name: Optional[str] = None, +) -> Optional[ContainerConfig]: + """Find container configuration, checking local then global.""" + # If container name is provided, search for it + if container_name: + # First check local deu.toml + local_toml = Path.cwd() / "deu.toml" + if local_toml.exists(): + try: + config = toml.loads(local_toml.read_text()) + if config["container"]["service"] == container_name: + return get_container_config() + except Exception: + pass + + # Then check global configs + global_dir = get_global_config_dir() + if global_dir.exists(): + for toml_file in global_dir.glob("*.toml"): + try: + config = toml.loads(toml_file.read_text()) + if config["container"]["service"] == container_name: + return ContainerConfig( + path=Path(config["container"]["path"]), + service=config["container"]["service"], + image=config["container"]["image"], + shell=ShellType[config["container"]["shell"]], + ) + except Exception: + continue + return None + + # If no container name, just check local deu.toml + local_toml = Path.cwd() / "deu.toml" + if local_toml.exists(): + return get_container_config() + return None -def handle_logs() -> None: - """Show container logs.""" - container_path = get_container_config().path - run_docker_compose_command(container_path, "logs -f") - - -def handle_stop() -> None: - """Stop the container.""" - container_path = get_container_config().path - run_docker_compose_command(container_path, "stop") - - -def handle_rm() -> None: - """Remove the container.""" - container_path = get_container_config().path - run_docker_compose_command(container_path, "down") - - -def create_dev_container(path: str, image: str, service_name: str) -> None: +def create_dev_container( + path: str, image: str, service_name: str, is_global: bool = False +) -> None: """Create a development container configuration.""" try: - container_path = Path(path) - validate_path(container_path) + if is_global: + # Create global config directory first + global_dir = get_global_config_dir() + global_dir.mkdir(parents=True, exist_ok=True) + container_path = global_dir / f".{service_name}" + toml_path = global_dir / f"{service_name}.toml" + else: + container_path = Path(path) + toml_path = Path.cwd() / "deu.toml" + validate_image(image) # Ensure image exists and detect shell @@ -264,7 +270,7 @@ def create_dev_container(path: str, image: str, service_name: str) -> None: ) sys.exit(1) - # Create the container directory if it doesn't exist + # Create the container directory container_path.mkdir(parents=True, exist_ok=True) # Create configurations @@ -278,7 +284,7 @@ def create_dev_container(path: str, image: str, service_name: str) -> None: # Write configurations write_config(container_path / "docker-compose.yml", compose_config, "yaml") - write_config(Path.cwd() / "deu.toml", toml_config, "toml") + write_config(toml_path, toml_config, "toml") except Exception as e: logger.error(f"Failed to create development container: {e}") @@ -302,6 +308,96 @@ def generate_service_name() -> str: return f"{random.choice(adjectives)}_{random.choice(nouns)}_{random_number}" +def handle_activate(container_name: Optional[str] = None) -> None: + """Activate the container shell.""" + config = find_container_config(container_name) + if not config: + if container_name: + logger.error(f"Container '{container_name}' not found locally or globally.") + else: + logger.error( + "No local container configuration found. Run 'deu init' first or specify a container name." + ) + sys.exit(1) + + print(f"Trying to activate container shell for service '{config.service}'...") + + # Check if container is running, start it if not + try: + ps_output = subprocess.run( + ["docker", "compose", "-f", str(config.path / "docker-compose.yml"), "ps"], + check=True, + capture_output=True, + text=True, + ) + if config.service not in ps_output.stdout: + logger.info(f"Container '{config.service}' is not running. Starting it...") + run_docker_compose_command(config.path, "up -d") + except subprocess.CalledProcessError: + logger.info(f"Container '{config.service}' is not running. Starting it...") + run_docker_compose_command(config.path, "up -d") + + # Use appropriate shell based on configuration + shell = "bash" if config.shell == ShellType.BASH else "sh" + run_docker_compose_command(config.path, f"exec {config.service} {shell}") + + +def handle_background(container_name: Optional[str] = None) -> None: + """Start the container in background mode.""" + config = find_container_config(container_name) + if not config: + if container_name: + logger.error(f"Container '{container_name}' not found locally or globally.") + else: + logger.error( + "No local container configuration found. Run 'deu init' first or specify a container name." + ) + sys.exit(1) + run_docker_compose_command(config.path, "up", detach=True) + + +def handle_logs(container_name: Optional[str] = None) -> None: + """Show container logs.""" + config = find_container_config(container_name) + if not config: + if container_name: + logger.error(f"Container '{container_name}' not found locally or globally.") + else: + logger.error( + "No local container configuration found. Run 'deu init' first or specify a container name." + ) + sys.exit(1) + run_docker_compose_command(config.path, "logs -f") + + +def handle_stop(container_name: Optional[str] = None) -> None: + """Stop the container.""" + config = find_container_config(container_name) + if not config: + if container_name: + logger.error(f"Container '{container_name}' not found locally or globally.") + else: + logger.error( + "No local container configuration found. Run 'deu init' first or specify a container name." + ) + sys.exit(1) + run_docker_compose_command(config.path, "stop") + + +def handle_rm(container_name: Optional[str] = None) -> None: + """Remove the container.""" + config = find_container_config(container_name) + if not config: + if container_name: + logger.error(f"Container '{container_name}' not found locally or globally.") + else: + logger.error( + "No local container configuration found. Run 'deu init' first or specify a container name." + ) + sys.exit(1) + run_docker_compose_command(config.path, "down") + + def main() -> None: """Main entry point for the script.""" parser = argparse.ArgumentParser( @@ -312,35 +408,69 @@ def main() -> None: # Init command init_parser = subparsers.add_parser("init", help="Initialize a new container") - init_parser.add_argument("path", help="Path to the .container directory") + init_parser.add_argument("path", nargs="?", help="Path to the .container directory") init_parser.add_argument("--image", required=True, help="Docker image to use") init_parser.add_argument( "--service", default=generate_service_name(), help="Name of the container service", ) + init_parser.add_argument( + "-g", + "--global", + dest="is_global", + action="store_true", + help="Create container in global config", + ) - # Other commands - subparsers.add_parser("activate", help="Activate container shell") - subparsers.add_parser("background", help="Start container in background") - subparsers.add_parser("logs", help="Show container logs") - subparsers.add_parser("stop", help="Stop container") - subparsers.add_parser("rm", help="Remove container") + # Activate command + activate_parser = subparsers.add_parser("activate", help="Activate container shell") + activate_parser.add_argument( + "container_name", nargs="?", help="Name of the container to activate" + ) + + # Background command + background_parser = subparsers.add_parser( + "background", help="Start container in background" + ) + background_parser.add_argument( + "container_name", nargs="?", help="Name of the container to start" + ) + + # Logs command + logs_parser = subparsers.add_parser("logs", help="Show container logs") + logs_parser.add_argument( + "container_name", nargs="?", help="Name of the container to show logs for" + ) + + # Stop command + stop_parser = subparsers.add_parser("stop", help="Stop container") + stop_parser.add_argument( + "container_name", nargs="?", help="Name of the container to stop" + ) + + # Remove command + rm_parser = subparsers.add_parser("rm", help="Remove container") + rm_parser.add_argument( + "container_name", nargs="?", help="Name of the container to remove" + ) args = parser.parse_args() if args.command == "init": - create_dev_container(args.path, args.image, args.service) + create_dev_container( + args.path or ".container", args.image, args.service, args.is_global + ) elif args.command == "activate": - handle_activate() + handle_activate(args.container_name) elif args.command == "background": - handle_background() + handle_background(args.container_name) elif args.command == "logs": - handle_logs() + handle_logs(args.container_name) elif args.command == "stop": - handle_stop() + handle_stop(args.container_name) elif args.command == "rm": - handle_rm() + handle_rm(args.container_name) else: parser.print_help() sys.exit(1)