#!/usr/bin/env python3 import sys import logging import subprocess from pathlib import Path from typing import Dict, Any, Optional import yaml import toml import argparse # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) def validate_path(path: Path) -> None: """Validate the provided path.""" if not path: raise ValueError("Path cannot be empty") if not path.is_absolute(): path = path.absolute() if not path.parent.exists(): raise FileNotFoundError(f"Parent directory {path.parent} does not exist") def validate_image(image: str) -> None: """Validate the Docker image name.""" if not image: raise ValueError("Docker image cannot be empty") # Basic validation for image format if not any(char in image for char in [':', '@']): logger.warning("No tag or digest specified for the image. Using 'latest' tag") def create_docker_compose_config(workspace_path: Path, image: str, service_name: str) -> Dict[str, Any]: """Create the Docker Compose configuration.""" return { "services": { service_name: { "image": image, "command": "tail -f /dev/null", # Keep container running with a shell "volumes": [f"{workspace_path.absolute()}:/workspace"], "working_dir": "/workspace", "stdin_open": True, "tty": True, "environment": { "TERM": "xterm-256color", }, "security_opt": ["seccomp=unconfined"], } } } def create_toml_config(container_path: Path, service_name: str) -> Dict[str, Any]: """Create the TOML configuration.""" return { "container": { "path": str(container_path.absolute()), "service": service_name, } } def write_config(path: Path, config: Dict[str, Any], config_type: str) -> None: """Write configuration to a file.""" try: if config_type == "yaml": path.write_text(yaml.dump(config, sort_keys=False)) elif config_type == "toml": path.write_text(toml.dumps(config)) logger.info(f"Created {config_type.upper()} file at: {path}") except Exception as e: logger.error(f"Failed to write {config_type.upper()} file: {e}") raise def run_docker_compose_command(container_path: Path, command: str, detach: bool = False) -> None: """Run a Docker Compose command.""" try: cmd = ["docker", "compose", "-f", str(container_path / "docker-compose.yml")] cmd.extend(command.split()) if detach: cmd.append("-d") subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: logger.error(f"Failed to execute Docker Compose command: {e}") sys.exit(1) def get_service_name(container_path: Path) -> str: """Get the service name from deu.toml.""" try: config = toml.loads((Path.cwd() / "deu.toml").read_text()) return config["container"]["service"] except Exception as e: logger.error(f"Failed to read service name from deu.toml: {e}") sys.exit(1) def create_dev_container(path: str, image: str, service_name: str) -> None: """ Create a development container configuration. Args: path: Path to the .container directory image: Docker image to use service_name: Name of the container service """ try: container_path = Path(path) validate_path(container_path) validate_image(image) # Create the container directory if it doesn't exist container_path.mkdir(parents=True, exist_ok=True) # Create configurations workspace_path = container_path.parent compose_config = create_docker_compose_config(workspace_path, image, service_name) toml_config = create_toml_config(container_path, service_name) # Write configurations write_config(container_path / "docker-compose.yml", compose_config, "yaml") write_config(Path.cwd() / "deu.toml", toml_config, "toml") except Exception as e: logger.error(f"Failed to create development container: {e}") sys.exit(1) def find_container_config() -> Optional[Path]: """Find the container configuration directory.""" current = Path.cwd() toml_path = current / "deu.toml" if toml_path.exists(): try: config = toml.loads(toml_path.read_text()) container_path = Path(config["container"]["path"]) if container_path.exists() and (container_path / "docker-compose.yml").exists(): return container_path except Exception as e: logger.error(f"Failed to read deu.toml: {e}") return None def handle_activate() -> None: """Activate the container shell.""" container_path = find_container_config() if not container_path: logger.error("No container configuration found. Run 'deu init' first.") sys.exit(1) service_name = get_service_name(container_path) print(f"Trying to activate container shell for service '{service_name}'...") # Check if container is running, start it if not try: ps_output = subprocess.run( ["docker", "compose", "-f", str(container_path / "docker-compose.yml"), "ps"], check=True, capture_output=True, text=True ) if service_name not in ps_output.stdout: logger.info(f"Container '{service_name}' is not running. Starting it...") run_docker_compose_command(container_path, "up -d") except subprocess.CalledProcessError: logger.info(f"Container '{service_name}' is not running. Starting it...") run_docker_compose_command(container_path, "up -d") # Now exec into the container run_docker_compose_command(container_path, f"exec {service_name} bash") def handle_background() -> None: """Start the container in background mode.""" container_path = find_container_config() if not container_path: logger.error("No container configuration found. Run 'deu init' first.") sys.exit(1) run_docker_compose_command(container_path, "up", detach=True) def handle_logs() -> None: """Show container logs.""" container_path = find_container_config() if not container_path: logger.error("No container configuration found. Run 'deu init' first.") sys.exit(1) run_docker_compose_command(container_path, "logs -f") def handle_stop() -> None: """Stop the container.""" container_path = find_container_config() if not container_path: logger.error("No container configuration found. Run 'deu init' first.") sys.exit(1) run_docker_compose_command(container_path, "stop") def handle_rm() -> None: """Remove the container.""" container_path = find_container_config() if not container_path: logger.error("No container configuration found. Run 'deu init' first.") sys.exit(1) run_docker_compose_command(container_path, "down") def main() -> None: """Main entry point for the script.""" parser = argparse.ArgumentParser( description="Development container utility", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) subparsers = parser.add_subparsers(dest="command", help="Command to execute") # 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("--image", required=True, help="Docker image to use") init_parser.add_argument("--service", default="dev", help="Name of the container service") # 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") args = parser.parse_args() if args.command == "init": create_dev_container(args.path, args.image, args.service) elif args.command == "activate": handle_activate() elif args.command == "background": handle_background() elif args.command == "logs": handle_logs() elif args.command == "stop": handle_stop() elif args.command == "rm": handle_rm() else: parser.print_help() sys.exit(1) if __name__ == "__main__": main()