From f216f374497be051c633da0a0f56e0a7fc04f71b Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 1 May 2025 10:56:21 +0200 Subject: [PATCH] Initial commit --- .gitignore | 11 +++ deu.py | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 .gitignore create mode 100755 deu.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cbbcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +testbed +__pycache__ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +docker-compose.yml +deu.toml diff --git a/deu.py b/deu.py new file mode 100755 index 0000000..2fe665b --- /dev/null +++ b/deu.py @@ -0,0 +1,250 @@ +#!/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()