From 70b4884199460b1460a3905d19cadaa7eedc615f Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Sep 2025 09:49:43 +0530 Subject: [PATCH 1/4] Initial DockyBot commit --- dockybot/__init__.py | 7 + dockybot/__main__.py | 11 + dockybot/cli.py | 166 +++++++++++++ dockybot/dagger_client_new.py | 106 ++++++++ dockybot/dagger_client_robust.py | 137 +++++++++++ dockybot/docker_runner.py | 410 +++++++++++++++++++++++++++++++ dockybot/platforms.py | 47 ++++ dockybot/scripts/alpine.sh | 26 ++ dockybot/scripts/centos.sh | 27 ++ dockybot/scripts/debian.sh | 27 ++ dockybot/scripts/ubuntu.sh | 47 ++++ dockybot_setup.py | 47 ++++ requirements.txt | 6 +- 13 files changed, 1063 insertions(+), 1 deletion(-) create mode 100644 dockybot/__init__.py create mode 100644 dockybot/__main__.py create mode 100644 dockybot/cli.py create mode 100644 dockybot/dagger_client_new.py create mode 100644 dockybot/dagger_client_robust.py create mode 100644 dockybot/docker_runner.py create mode 100644 dockybot/platforms.py create mode 100644 dockybot/scripts/alpine.sh create mode 100644 dockybot/scripts/centos.sh create mode 100644 dockybot/scripts/debian.sh create mode 100644 dockybot/scripts/ubuntu.sh create mode 100644 dockybot_setup.py diff --git a/dockybot/__init__.py b/dockybot/__init__.py new file mode 100644 index 00000000..37b78937 --- /dev/null +++ b/dockybot/__init__.py @@ -0,0 +1,7 @@ +""" +DockyBot - Ubuntu testing with Dagger + +Simplified tool to replicate PR pipeline locally on Ubuntu. +""" + +__version__ = "0.1.0" \ No newline at end of file diff --git a/dockybot/__main__.py b/dockybot/__main__.py new file mode 100644 index 00000000..28e55b6c --- /dev/null +++ b/dockybot/__main__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +DockyBot package main entry point + +Allows running: python -m dockybot +""" + +from .cli import app + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/dockybot/cli.py b/dockybot/cli.py new file mode 100644 index 00000000..d15c86ef --- /dev/null +++ b/dockybot/cli.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +DockyBot CLI - Minimal Docker abstraction for multi-platform testing. +""" + +import typer +from rich.console import Console +from rich.table import Table +from .docker_runner import DockerRunner +from .platforms import PLATFORMS, list_platforms + +app = typer.Typer(help="DockyBot - Run tests across platforms with Docker") +console = Console() + +@app.command() +def test( + platform: str = typer.Argument(..., help="Platform to test on"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output") +): + """Run tests on specified platform.""" + if platform not in PLATFORMS: + console.print(f"[red]Error:[/red] Unknown platform '{platform}'") + console.print(f"Available platforms: {', '.join(PLATFORMS.keys())}") + raise typer.Exit(1) + + runner = DockerRunner() + success = runner.run_platform_test(platform, verbose=verbose) + + if not success: + raise typer.Exit(1) + +@app.command() +def platforms(): + """List available platforms.""" + table = Table(title="Available Platforms") + table.add_column("Platform", style="cyan") + table.add_column("Name", style="green") + table.add_column("Description", style="white") + + for platform_id, config in list_platforms().items(): + table.add_row(platform_id, config['name'], config['description']) + + console.print(table) + +@app.command() +def test_all( + verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output") +): + """Run tests on all platforms.""" + runner = DockerRunner() + results = {} + + for platform in PLATFORMS.keys(): + console.print(f"\n[cyan]Testing {platform}...[/cyan]") + results[platform] = runner.run_platform_test(platform, verbose=verbose) + + # Summary + console.print("\n[bold]Test Results Summary:[/bold]") + for platform, success in results.items(): + status = "[green]โœ“ PASSED[/green]" if success else "[red]โœ— FAILED[/red]" + console.print(f" {platform}: {status}") + + failed_count = sum(1 for success in results.values() if not success) + if failed_count > 0: + console.print(f"\n[red]{failed_count} platform(s) failed[/red]") + raise typer.Exit(1) + else: + console.print("\n[green]All platforms passed![/green]") + +if __name__ == "__main__": + app() + +import typer +from typing import Optional +from rich.console import Console +from rich.panel import Panel +from pathlib import Path + +from .docker_client import DockerClient + +app = typer.Typer( + name="dockybot", + help="๐Ÿค– DockyBot - DevOps-as-Code testing tool using Docker", + no_args_is_help=True, + rich_markup_mode="rich" +) + +console = Console() + + +@app.command() +def test( + cache: bool = typer.Option( + True, + "--cache/--no-cache", + help="Enable/disable pip and build artifact caching" + ), + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + help="Enable verbose output" + ) +): + """ + Run tests on Ubuntu using Docker (replicates exact PR pipeline steps). + """ + console.print(Panel.fit( + f"๐Ÿš€ [bold blue]DockyBot Ubuntu Test Runner[/bold blue]\n" + f"Platform: Ubuntu 22.04\n" + f"Caching: {'โœ… Enabled' if cache else 'โŒ Disabled'}", + border_style="blue" + )) + + console.print("๐Ÿ”„ [bold yellow]Testing on Ubuntu...[/bold yellow]") + + # Create Docker client and run tests + try: + client = DockerClient(verbose=verbose, cache_enabled=cache) + success = client.run_tests(platform="ubuntu") + + if success: + console.print("[bold green]โœ… All tests passed![/bold green]") + else: + console.print("[bold red]โŒ Tests failed![/bold red]") + raise typer.Exit(1) + + except Exception as e: + console.print(f"[red]โŒ Error: {e}[/red]") + raise typer.Exit(1) + + +@app.command() +def clean(): + """ + Clean up Docker cache and containers. + """ + console.print("๐Ÿงน [bold yellow]Cleaning Docker cache...[/bold yellow]") + + try: + client = DockerClient() + client.clean_cache() + except Exception as e: + console.print(f"[red]โŒ Error cleaning cache: {e}[/red]") + raise typer.Exit(1) + + +@app.command() +def version(): + """ + Show DockyBot version information. + """ + try: + from . import __version__ + console.print(f"DockyBot v{__version__} (Docker-based)") + except ImportError: + console.print("DockyBot v1.0.0 (Docker-based)") + + +def main(): + """Entry point for the CLI.""" + app() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/dockybot/dagger_client_new.py b/dockybot/dagger_client_new.py new file mode 100644 index 00000000..e34369f6 --- /dev/null +++ b/dockybot/dagger_client_new.py @@ -0,0 +1,106 @@ +""" +DockyBot Dagger Client - Ubuntu test pipeline + +Clean implementation that uses localhost SQL Server. +""" + +import asyncio +import dagger +from rich.console import Console +from rich.panel import Panel + +console = Console() + + +class DaggerClient: + """Dagger client for running Ubuntu test pipeline.""" + + def __init__(self, verbose: bool = False, cache_enabled: bool = True): + self.verbose = verbose + self.cache_enabled = cache_enabled + + def run_tests(self, platform: str = "ubuntu") -> bool: + """Entry point for running tests on a given platform (only ubuntu supported).""" + if platform != "ubuntu": + raise ValueError("Only Ubuntu platform is supported for now") + return asyncio.run(self._run_tests_async()) + + async def _run_tests_async(self) -> bool: + """Run the Ubuntu PR pipeline inside Dagger with localhost SQL Server.""" + + async with dagger.Connection(dagger.Config(log_output=None)) as client: + try: + console.print("[bold green]๐Ÿณ Starting Dagger engine...[/bold green]") + + # Workspace + workspace = client.host().directory( + ".", + exclude=[".git/", "__pycache__/", "*.pyc", "venv/", "build/", "dist/"], + ) + + # Create Ubuntu container for testing + console.print("๐Ÿง Creating Ubuntu test container...") + container = ( + client.container() + .from_("ubuntu:22.04") + .with_workdir("/workspace") + .with_directory("/workspace", workspace) + ) + + # Run complete pipeline in one step + console.print("๐Ÿ”„ Running complete pipeline...") + + pipeline_script = """ +set -euxo pipefail + +# Install system dependencies +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y python3 python3-pip python3-venv cmake build-essential curl wget gnupg python3-dev pybind11-dev + +# Install ODBC driver +curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb +dpkg -i packages-microsoft-prod.deb || true +apt-get update +ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 unixodbc-dev + +# Setup Python environment +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +# Build C++ extensions +cd mssql_python/pybind +chmod +x build.sh +./build.sh +cd /workspace + +# Run tests with localhost SQL Server +export DB_CONNECTION_STRING="Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;" +python -m pytest -v --tb=short --cache-clear +""" + + result = await container.with_exec(["bash", "-c", pipeline_script]).sync() + + console.print( + Panel.fit( + "[bold green]๐ŸŽ‰ All tests completed successfully![/bold green]", + border_style="green", + title="Success", + ) + ) + return True + + except dagger.ExecError as e: + console.print(f"[red]โŒ Pipeline failed[/red]") + console.print(f"[yellow]STDOUT:[/yellow]\n{e.stdout}") + console.print(f"[red]STDERR:[/red]\n{e.stderr}") + return False + except Exception as e: + console.print(f"[red]โŒ Unexpected error: {e}[/red]") + return False + + def clean_cache(self): + """Placeholder for cleaning caches (Dagger manages volumes internally).""" + console.print("๐Ÿงน Cache cleanup not implemented โ€” use `dagger engine reset` if needed.") \ No newline at end of file diff --git a/dockybot/dagger_client_robust.py b/dockybot/dagger_client_robust.py new file mode 100644 index 00000000..a68514d7 --- /dev/null +++ b/dockybot/dagger_client_robust.py @@ -0,0 +1,137 @@ +""" +DockyBot Dagger Client - Ubuntu test pipeline + +Robust implementation with proper timeout handling. +""" + +import asyncio +import signal +import sys +import dagger +from rich.console import Console +from rich.panel import Panel + +console = Console() + + +class DaggerClient: + """Dagger client for running Ubuntu test pipeline.""" + + def __init__(self, verbose: bool = False, cache_enabled: bool = True): + self.verbose = verbose + self.cache_enabled = cache_enabled + + def run_tests(self, platform: str = "ubuntu") -> bool: + """Entry point for running tests on a given platform (only ubuntu supported).""" + if platform != "ubuntu": + raise ValueError("Only Ubuntu platform is supported for now") + + # Set up signal handler for graceful termination + def signal_handler(signum, frame): + console.print("\n[yellow]โš ๏ธ Received interrupt signal. Cleaning up...[/yellow]") + sys.exit(1) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + return asyncio.run(asyncio.wait_for(self._run_tests_async(), timeout=1800)) # 30 min timeout + except asyncio.TimeoutError: + console.print("[red]โŒ Pipeline timed out after 30 minutes[/red]") + return False + except KeyboardInterrupt: + console.print("\n[yellow]โš ๏ธ Operation cancelled by user[/yellow]") + return False + + async def _run_tests_async(self) -> bool: + """Run the Ubuntu PR pipeline inside Dagger with localhost SQL Server.""" + + # Simple non-hanging approach - no persistent connections + try: + console.print("[bold green]๐Ÿณ Starting Dagger engine...[/bold green]") + + # Create connection with shorter timeout + config = dagger.Config(log_output=sys.stderr, timeout=600) # 10 min timeout + + async with dagger.Connection(config) as client: + # Workspace + workspace = client.host().directory( + ".", + exclude=[".git/", "__pycache__/", "*.pyc", "venv/", "build/", "dist/"], + ) + + # Create and run container in one shot + console.print("๐Ÿง Running Ubuntu pipeline...") + + # Simplified pipeline script + pipeline_script = ''' +#!/bin/bash +set -euo pipefail + +echo "๐Ÿ”ง Installing system dependencies..." +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq python3 python3-pip python3-venv cmake build-essential curl wget gnupg python3-dev + +echo "๐Ÿ“€ Installing ODBC driver..." +curl -sSL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - +curl -sSL https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list +apt-get update -qq +ACCEPT_EULA=Y apt-get install -y -qq msodbcsql18 mssql-tools18 unixodbc-dev + +echo "๐Ÿ Setting up Python environment..." +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip -q +pip install -r requirements.txt -q + +echo "๐Ÿ”จ Building C++ extensions..." +cd mssql_python/pybind +chmod +x build.sh +./build.sh +cd /workspace + +echo "๐Ÿงช Running tests..." +export DB_CONNECTION_STRING="Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;" +python -m pytest tests/ -v --tb=short --maxfail=5 -x + +echo "โœ… Pipeline completed successfully!" +''' + + # Execute with timeout + result = await asyncio.wait_for( + client.container() + .from_("ubuntu:22.04") + .with_workdir("/workspace") + .with_directory("/workspace", workspace) + .with_exec(["bash", "-c", pipeline_script]) + .sync(), + timeout=1200 # 20 minute timeout for the container execution + ) + + console.print( + Panel.fit( + "[bold green]๐ŸŽ‰ All tests completed successfully![/bold green]", + border_style="green", + title="Success", + ) + ) + return True + + except asyncio.TimeoutError: + console.print("[red]โŒ Container execution timed out[/red]") + return False + except dagger.ExecError as e: + console.print(f"[red]โŒ Pipeline failed[/red]") + if self.verbose and e.stdout: + console.print(f"[yellow]STDOUT:[/yellow]\n{e.stdout[:2000]}...") + if e.stderr: + console.print(f"[red]STDERR:[/red]\n{e.stderr[:1000]}...") + return False + except Exception as e: + console.print(f"[red]โŒ Unexpected error: {e}[/red]") + return False + + def clean_cache(self): + """Clean Dagger cache.""" + console.print("๐Ÿงน To clean Dagger cache, run: dagger engine reset") \ No newline at end of file diff --git a/dockybot/docker_runner.py b/dockybot/docker_runner.py new file mode 100644 index 00000000..b8fe0923 --- /dev/null +++ b/dockybot/docker_runner.py @@ -0,0 +1,410 @@ +""" +Minimal Docker abstraction f # Print welcome message + console.print(f"\n๐Ÿš€ [bold green]Starting DockyBot Test Suite[/bold green]") + console.print(f"๐Ÿณ [bold blue]Platform:[/bold blue] {platfo elif "requirement elif "building" in line_lower and ("c++" in line_lower or "extensions" in line_lower or "pybind" in line_lower): + return "๐Ÿ”จ Building C++ extensions", 5 + elif ("running tests" in line_lower or "pytest" in line_lower or + "tests/test_" in line_lower or "passed" in line_lower or + "failed" in line_lower or "skipped" in line_lower or + "test session starts" in line_lower or "collected" in line_lower): + return "๐Ÿงช Running tests", 6t" in line_lower or "pip install" in line_lower: + return "๐Ÿ“ฆ Installing Python packages", 4 + elif "building" in line_lower and ("c++" in line_lower or "extensions" in line_lower or "pybind" in line_lower): + return "๐Ÿ”จ Building C++ extensions", 5 + elif any(test_indicator in line_lower for test_indicator in [ + "running tests", "pytest", "test session starts", "collected", + "tests/test_", "passed", "failed", "skipped", "::test_" + ]): + return "๐Ÿงช Running tests", 6 + elif "completed successfully" in line_lower or "test pipeline completed" in line_lower: + return "โœ… Tests completed", 7title()}") + console.print(f"โฐ [bold yellow]Starting at:[/bold yellow] {console._environ.get('TZ', 'UTC')} time") + console.print(f"๐Ÿ”— [bold cyan]Database:[/bold cyan] SQL Server via host.docker.internal") + console.print()nning tests across platforms. +""" + +import docker +import tempfile +import os +from pathlib import Path +from rich.console import Console +from rich.live import Live +from rich.panel import Panel +from rich.text import Text +from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn +from rich.rule import Rule +from rich.columns import Columns +from .platforms import get_platform_script +import time + +console = Console() + + +class DockerRunner: + """Minimal Docker abstraction for cross-platform testing.""" + + def __init__(self, verbose: bool = False): + self.verbose = verbose + self.client = docker.from_env() + self.workspace = Path.cwd() + + def run_platform(self, platform_name: str, script_content: str) -> bool: + """Run a platform test script in Docker.""" + + # Print welcome message + console.print(f"\n๏ฟฝ [bold green]Starting DockyBot Test Suite[/bold green]") + console.print(f"๐Ÿณ [bold blue]Platform:[/bold blue] {platform_name.title()}") + console.print() + + # Create temp script + with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f: + f.write(script_content) + script_path = f.name + + try: + os.chmod(script_path, 0o755) + + # Get platform config + platform_config = self._get_platform_config(platform_name) + + # Prepare environment variables + environment = { + 'DB_CONNECTION_STRING': 'Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;', + 'PYTHONPATH': '/workspace', + 'DEBIAN_FRONTEND': 'noninteractive', + 'TZ': 'UTC' + } + + # Override with any environment variables from host if they exist + if 'DB_CONNECTION_STRING' in os.environ: + environment['DB_CONNECTION_STRING'] = os.environ['DB_CONNECTION_STRING'] + + # Run container + container = self.client.containers.run( + platform_config['image'], + command=platform_config['command'] + [f"/tmp/test.sh"], + volumes={ + str(self.workspace): {"bind": "/workspace", "mode": "rw"}, + script_path: {"bind": "/tmp/test.sh", "mode": "ro"} + }, + working_dir="/workspace", + environment=environment, + remove=True, + detach=True, + **platform_config.get('docker_options', {}) + ) + + # Stream output + return self._stream_container_output(container, platform_name) + + finally: + if os.path.exists(script_path): + os.unlink(script_path) + + def _get_platform_config(self, platform_name: str) -> dict: + """Get Docker configuration for platform.""" + configs = { + 'ubuntu': { + 'image': 'ubuntu:22.04', + 'command': ['bash'], + 'docker_options': { + 'extra_hosts': {"host.docker.internal": "host-gateway"} + } + }, + 'alpine': { + 'image': 'alpine:3.18', + 'command': ['sh'], + 'docker_options': { + 'extra_hosts': {"host.docker.internal": "host-gateway"} + } + }, + 'centos': { + 'image': 'centos:7', + 'command': ['bash'], + 'docker_options': { + 'extra_hosts': {"host.docker.internal": "host-gateway"} + } + }, + 'debian': { + 'image': 'debian:11', + 'command': ['bash'], + 'docker_options': { + 'extra_hosts': {"host.docker.internal": "host-gateway"} + } + } + } + + if platform_name not in configs: + raise ValueError(f"Unknown platform: {platform_name}") + + return configs[platform_name] + + def _stream_container_output(self, container, platform_name: str) -> bool: + """Stream container output with live updates and beautiful formatting.""" + + output_lines = [] + current_step = "Starting" + step_count = 0 + total_steps = 8 # Updated number of major steps including DB verification + start_time = time.time() + + # Print header + console.print(Rule(f"[bold blue]๐Ÿณ Running {platform_name.title()} Tests[/bold blue]")) + + with Progress( + SpinnerColumn(), + TextColumn("[bold blue]{task.description}", justify="left"), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%", justify="center"), + TextColumn("[dim]{task.fields[elapsed]}", justify="right"), + console=console, + expand=True + ) as progress: + + task = progress.add_task( + f"[cyan]Initializing {platform_name}...", + total=total_steps, + elapsed="0:00" + ) + + try: + for log_line in container.logs(stream=True, follow=True): + line = log_line.decode('utf-8').strip() + if line: + output_lines.append(line) + + # Update progress based on content + new_step, step_progress = self._extract_step_info(line) + if new_step != current_step and step_progress > 0: + current_step = new_step + step_count = step_progress # Use the actual step number, not increment + + # Calculate elapsed time + elapsed_seconds = int(time.time() - start_time) + elapsed_str = f"{elapsed_seconds // 60}:{elapsed_seconds % 60:02d}" + + progress.update( + task, + completed=step_count, + description=f"[cyan]{current_step}", + elapsed=elapsed_str + ) + + # Format and display output + self._display_formatted_line(line) + + # Check result + result = container.wait() + success = result['StatusCode'] == 0 + + # Final elapsed time + elapsed_seconds = int(time.time() - start_time) + elapsed_str = f"{elapsed_seconds // 60}:{elapsed_seconds % 60:02d}" + + # Complete progress + progress.update( + task, + completed=total_steps, + description="[green]โœ… Completed" if success else "[red]โŒ Failed", + elapsed=elapsed_str + ) + + except Exception as e: + elapsed_seconds = int(time.time() - start_time) + elapsed_str = f"{elapsed_seconds // 60}:{elapsed_seconds % 60:02d}" + progress.update(task, description="[red]โŒ Error occurred", elapsed=elapsed_str) + console.print(f"[red]๐Ÿ’ฅ Error streaming logs: {e}[/red]") + return False + + # Print summary (outside the progress context to avoid overlap) + elapsed_seconds = int(time.time() - start_time) + self._print_test_summary(platform_name, success, len(output_lines), elapsed_seconds) + + return success + + def _create_status_panel(self, platform_name: str, status: str) -> Panel: + """Create status panel for live updates.""" + content = Text() + content.append(f"Platform: ", style="bold") + content.append(f"{platform_name}\n", style="blue") + content.append(f"Status: ", style="bold") + content.append(status, style="green" if "โœ…" in status else "yellow" if "..." in status else "red") + + return Panel(content, title="DockyBot", border_style="blue") + + def _extract_step_info(self, line: str) -> tuple[str, int]: + """Extract current step and progress from log line.""" + line_lower = line.lower() + + if ("apt-get update" in line_lower or + "installing system dependencies" in line_lower or + ("install" in line_lower and "dependencies" in line_lower)): + return "๐Ÿ“ฆ Installing system dependencies", 1 + elif ("microsoft odbc" in line_lower or "msodbcsql" in line_lower or + "packages-microsoft-prod" in line_lower): + return "๐Ÿ“€ Installing Microsoft ODBC Driver", 2 + elif "python" in line_lower and ("setup" in line_lower or "environment" in line_lower or "venv" in line_lower): + return "๐Ÿ Setting up Python environment", 3 + elif "requirements.txt" in line_lower or "pip install" in line_lower: + return "๏ฟฝ Installing Python packages", 4 + elif "building" in line_lower and ("c++" in line_lower or "extensions" in line_lower or "pybind" in line_lower): + return "๏ฟฝ Building C++ extensions", 5 + elif "running tests" in line_lower or "pytest" in line_lower: + return "๐Ÿงช Running tests", 6 + elif "completed successfully" in line_lower or "test pipeline completed" in line_lower: + return "โœ… Tests completed", 7 + else: + return "๐Ÿ”„ Processing", 0 + + def _extract_status(self, line: str) -> str: + """Extract current status from log line (legacy method for compatibility).""" + step, _ = self._extract_step_info(line) + return step + + def _display_formatted_line(self, line: str) -> None: + """Display a formatted log line with appropriate styling.""" + line_lower = line.lower() + line_stripped = line.strip() + + # Skip empty lines and less important output + if not line_stripped or self._should_skip_line(line): + return + + # Test results (highest priority - check these first) + if " passed " in line_lower or line_lower.endswith(" passed"): + console.print(f"[green]โœ… {line}[/green]") + elif " failed " in line_lower or line_lower.endswith(" failed"): + console.print(f"[red]โŒ {line}[/red]") + elif " skipped " in line_lower or line_lower.endswith(" skipped"): + console.print(f"[yellow]โš ๏ธ {line}[/yellow]") + + # Database connection messages + elif any(word in line_lower for word in ['db_connection_string', 'database connection', 'connection target']): + console.print(f"[cyan]๐Ÿ”— {line}[/cyan]") + + # Installation success messages (check before error patterns) + elif any(phrase in line_lower for phrase in [ + 'successfully installed', 'successfully uninstalled', 'successfully', + 'setting up', 'processing triggers', 'created symlink', + 'collected packages', 'downloading', 'requirement already satisfied' + ]): + console.print(f"[green]โœ… {line}[/green]") + + # Package installation messages + elif any(phrase in line_lower for phrase in [ + 'installing collected packages', 'collecting', 'unpacking', 'preparing to unpack', + 'selecting previously unselected', 'installing system dependencies', + 'installing microsoft odbc', 'installing python packages' + ]): + console.print(f"[cyan]๐Ÿ“ฆ {line}[/cyan]") + + # Actual error conditions (very specific patterns) + elif any(pattern in line_lower for pattern in [ + 'fatal error', 'compilation failed', 'build failed', 'installation failed', + 'command not found', 'no such file or directory', 'permission denied', + 'connection refused', 'timeout error', 'cannot connect' + ]) and not any(success_word in line_lower for success_word in ['successfully', 'completed']): + console.print(f"[red]โŒ {line}[/red]") + + # Warning messages (but not system messages) + elif line_lower.startswith('warning:') or 'update-alternatives: warning' in line_lower: + console.print(f"[yellow]โš ๏ธ {line}[/yellow]") + + # Test execution messages + elif any(phrase in line_lower for phrase in ['running tests', 'pytest']) and 'passed' not in line_lower: + console.print(f"[magenta]๐Ÿงช {line}[/magenta]") + + # Building/compilation messages + elif any(phrase in line_lower for phrase in ['building', 'compiling', 'linking']) and 'failed' not in line_lower: + console.print(f"[cyan]๐Ÿ”จ {line}[/cyan]") + + # Important system messages + elif self._is_important_line(line): + console.print(f"[blue]โ„น๏ธ {line}[/blue]") + + # Verbose-only messages + elif self.verbose: + console.print(f"[dim] {line}[/dim]") + + def _should_skip_line(self, line: str) -> bool: + """Check if line should be skipped entirely.""" + line_lower = line.lower() + + # Skip very verbose package management output + skip_patterns = [ + 'debconf:', 'unable to initialize frontend:', 'dpkg-preconfigure:', + 'reading package lists', 'building dependency tree', + 'reading state information', 'the following new packages', + 'get:', 'hit:', 'ign:', 'reading changelogs', + 'reading database ...', '(reading database', 'files and directories currently installed', + 'preparing to unpack', 'unpacking', 'selecting previously unselected', + 'processing triggers for', 'invoke-rc.d:', 'created symlink', + 'update-alternatives: using', 'no schema files found', + 'first installation detected', 'checking nss setup', + 'current default time zone' + ] + + # Skip if line matches any skip pattern + if any(pattern in line_lower for pattern in skip_patterns): + return True + + # Skip very short or empty lines + if len(line.strip()) < 3: + return True + + # Skip lines that are just progress indicators + if line.strip().startswith('โ”') or line.strip().startswith('โ”‚'): + return True + + return False + + def _is_important_line(self, line: str) -> bool: + """Check if line should be shown in non-verbose mode.""" + important_keywords = [ + "Installing", "Building", "Testing", "Running", "โœ…", "โŒ", + "ERROR", "FAILED", "SUCCESS", "Started", "Finished", + "===", "collected", "warnings summary" + ] + return any(keyword in line for keyword in important_keywords) + + def _print_test_summary(self, platform_name: str, success: bool, log_lines: int, elapsed_seconds: int) -> None: + """Print a formatted test summary.""" + console.print() + console.print(Rule("[bold]Test Summary[/bold]")) + + status_color = "green" if success else "red" + status_icon = "โœ…" if success else "โŒ" + status_text = "PASSED" if success else "FAILED" + + elapsed_str = f"{elapsed_seconds // 60}:{elapsed_seconds % 60:02d}" + + summary_panel = Panel( + f"[bold]Platform:[/bold] {platform_name.title()}\n" + f"[bold]Status:[/bold] [{status_color}]{status_icon} {status_text}[/{status_color}]\n" + f"[bold]Duration:[/bold] {elapsed_str}\n" + f"[bold]Log Lines:[/bold] {log_lines}", + title="๐Ÿค– DockyBot Results", + border_style=status_color + ) + + console.print(summary_panel) + console.print() + + def run_platform_test(self, platform: str, verbose: bool = False) -> bool: + """Run platform test using the predefined test script.""" + self.verbose = verbose + + try: + # Get the test script for this platform + script_content = get_platform_script(platform) + + # Run the platform test + return self.run_platform(platform, script_content) + + except Exception as e: + console.print(f"[red]๐Ÿ’ฅ Error running {platform} test: {e}[/red]") + return False + + def list_platforms(self) -> list: + """List available platforms.""" + return ['ubuntu', 'alpine', 'centos', 'debian'] \ No newline at end of file diff --git a/dockybot/platforms.py b/dockybot/platforms.py new file mode 100644 index 00000000..d9136451 --- /dev/null +++ b/dockybot/platforms.py @@ -0,0 +1,47 @@ +""" +Platform definitions and test scripts for DockyBot. +""" + +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent / "scripts" + +# Supported platforms +PLATFORMS = { + 'ubuntu': { + 'name': 'Ubuntu 22.04', + 'script': 'ubuntu.sh', + 'description': 'Standard Ubuntu with apt package manager' + }, + 'alpine': { + 'name': 'Alpine Linux 3.18', + 'script': 'alpine.sh', + 'description': 'Lightweight Alpine with apk package manager' + }, + 'centos': { + 'name': 'CentOS 7', + 'script': 'centos.sh', + 'description': 'Enterprise CentOS with yum package manager' + }, + 'debian': { + 'name': 'Debian 11', + 'script': 'debian.sh', + 'description': 'Debian stable with apt package manager' + } +} + +def get_platform_script(platform_name: str) -> str: + """Get the test script content for a platform.""" + if platform_name not in PLATFORMS: + raise ValueError(f"Unknown platform: {platform_name}. Available: {list(PLATFORMS.keys())}") + + script_file = SCRIPT_DIR / PLATFORMS[platform_name]['script'] + + if not script_file.exists(): + raise FileNotFoundError(f"Script not found: {script_file}") + + return script_file.read_text() + +def list_platforms() -> dict: + """List all available platforms.""" + return PLATFORMS \ No newline at end of file diff --git a/dockybot/scripts/alpine.sh b/dockybot/scripts/alpine.sh new file mode 100644 index 00000000..f4346296 --- /dev/null +++ b/dockybot/scripts/alpine.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -euo pipefail + +echo "Starting Alpine Linux test pipeline..." + +# Install system dependencies +apk update +apk add --no-cache python3 py3-pip python3-dev cmake make gcc g++ libc-dev curl wget gnupg unixodbc-dev + +echo "Setting up Python environment..." +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +echo "Building C++ extensions..." +cd mssql_python/pybind +chmod +x build.sh +./build.sh + +echo "Running tests..." +cd /workspace +source /opt/venv/bin/activate +python -m pytest tests/ -v + +echo "Alpine test pipeline completed successfully!" \ No newline at end of file diff --git a/dockybot/scripts/centos.sh b/dockybot/scripts/centos.sh new file mode 100644 index 00000000..1071eb32 --- /dev/null +++ b/dockybot/scripts/centos.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +echo "Starting CentOS 7 test pipeline..." + +# Install system dependencies +yum update -y +yum groupinstall -y "Development Tools" +yum install -y python3 python3-pip python3-devel cmake curl wget unixodbc-devel + +echo "Setting up Python environment..." +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +echo "Building C++ extensions..." +cd mssql_python/pybind +chmod +x build.sh +./build.sh + +echo "Running tests..." +cd /workspace +source /opt/venv/bin/activate +python -m pytest tests/ -v + +echo "CentOS test pipeline completed successfully!" \ No newline at end of file diff --git a/dockybot/scripts/debian.sh b/dockybot/scripts/debian.sh new file mode 100644 index 00000000..3a731acf --- /dev/null +++ b/dockybot/scripts/debian.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +echo "Starting Debian 11 test pipeline..." + +# Install system dependencies +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y python3 python3-pip python3-venv python3-dev cmake build-essential curl wget gnupg unixodbc-dev + +echo "Setting up Python environment..." +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +echo "Building C++ extensions..." +cd mssql_python/pybind +chmod +x build.sh +./build.sh + +echo "Running tests..." +cd /workspace +source /opt/venv/bin/activate +python -m pytest tests/ -v + +echo "Debian test pipeline completed successfully!" \ No newline at end of file diff --git a/dockybot/scripts/ubuntu.sh b/dockybot/scripts/ubuntu.sh new file mode 100644 index 00000000..730d145d --- /dev/null +++ b/dockybot/scripts/ubuntu.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +echo "Starting Ubuntu 22.04 test pipeline..." + +# Install system dependencies +export DEBIAN_FRONTEND=noninteractive +export TZ=UTC +ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +apt-get update && apt-get install -y python3 python3-pip python3-venv python3-full cmake curl wget gnupg software-properties-common build-essential python3-dev pybind11-dev + +echo "Installing Microsoft ODBC Driver..." +curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb +dpkg -i packages-microsoft-prod.deb || true +rm packages-microsoft-prod.deb +apt-get update +ACCEPT_EULA=Y apt-get install -y msodbcsql18 +ACCEPT_EULA=Y apt-get install -y mssql-tools18 +apt-get install -y unixodbc-dev + +echo "Setting up Python environment..." +python3 -m venv /opt/venv +source /opt/venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +echo "Verifying database connection setup..." +if [ -z "${DB_CONNECTION_STRING:-}" ]; then + echo "โŒ Warning: DB_CONNECTION_STRING environment variable is not set!" + exit 1 +else + echo "โœ… DB_CONNECTION_STRING is configured" + # Print first part of connection string for verification (without password) + echo "Connection target: $(echo "$DB_CONNECTION_STRING" | grep -o 'Server=[^;]*')" +fi + +echo "Building C++ extensions..." +cd mssql_python/pybind +chmod +x build.sh +./build.sh + +echo "Running tests..." +cd /workspace +source /opt/venv/bin/activate +python -m pytest tests/ -v + +echo "Ubuntu test pipeline completed successfully!" \ No newline at end of file diff --git a/dockybot_setup.py b/dockybot_setup.py new file mode 100644 index 00000000..9468883f --- /dev/null +++ b/dockybot_setup.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Setup script for DockyBot package +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read README for long description +readme_path = Path(__file__).parent / "dockybot" / "README.md" +long_description = "" +if readme_path.exists(): + long_description = readme_path.read_text(encoding="utf-8") + +setup( + name="dockybot", + version="0.1.0", + description="DevOps-as-Code CLI for cross-platform testing with Dagger", + long_description=long_description, + long_description_content_type="text/markdown", + author="Microsoft", + packages=find_packages(), + python_requires=">=3.8", + install_requires=[ + "typer[all]>=0.9.0", + "dagger-io>=0.9.0", + "rich>=13.0.0", + ], + entry_points={ + "console_scripts": [ + "dockybot=dockybot.cli:main", + ], + }, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Testing", + "Topic :: System :: Systems Administration", + ], +) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5abf13dc..bb6d10d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,8 @@ pybind11 coverage unittest-xml-reporting setuptools -psutil \ No newline at end of file +psutil +# DockyBot dependencies +typer[all]>=0.9.0 +docker>=6.0.0 +rich>=13.0.0 \ No newline at end of file From 59e05b144c2b47a50b841864802a5f6791900103 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Sep 2025 09:50:21 +0530 Subject: [PATCH 2/4] delete irrelevant stuff --- dockybot/dagger_client_new.py | 106 ------------------------ dockybot/dagger_client_robust.py | 137 ------------------------------- 2 files changed, 243 deletions(-) delete mode 100644 dockybot/dagger_client_new.py delete mode 100644 dockybot/dagger_client_robust.py diff --git a/dockybot/dagger_client_new.py b/dockybot/dagger_client_new.py deleted file mode 100644 index e34369f6..00000000 --- a/dockybot/dagger_client_new.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -DockyBot Dagger Client - Ubuntu test pipeline - -Clean implementation that uses localhost SQL Server. -""" - -import asyncio -import dagger -from rich.console import Console -from rich.panel import Panel - -console = Console() - - -class DaggerClient: - """Dagger client for running Ubuntu test pipeline.""" - - def __init__(self, verbose: bool = False, cache_enabled: bool = True): - self.verbose = verbose - self.cache_enabled = cache_enabled - - def run_tests(self, platform: str = "ubuntu") -> bool: - """Entry point for running tests on a given platform (only ubuntu supported).""" - if platform != "ubuntu": - raise ValueError("Only Ubuntu platform is supported for now") - return asyncio.run(self._run_tests_async()) - - async def _run_tests_async(self) -> bool: - """Run the Ubuntu PR pipeline inside Dagger with localhost SQL Server.""" - - async with dagger.Connection(dagger.Config(log_output=None)) as client: - try: - console.print("[bold green]๐Ÿณ Starting Dagger engine...[/bold green]") - - # Workspace - workspace = client.host().directory( - ".", - exclude=[".git/", "__pycache__/", "*.pyc", "venv/", "build/", "dist/"], - ) - - # Create Ubuntu container for testing - console.print("๐Ÿง Creating Ubuntu test container...") - container = ( - client.container() - .from_("ubuntu:22.04") - .with_workdir("/workspace") - .with_directory("/workspace", workspace) - ) - - # Run complete pipeline in one step - console.print("๐Ÿ”„ Running complete pipeline...") - - pipeline_script = """ -set -euxo pipefail - -# Install system dependencies -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install -y python3 python3-pip python3-venv cmake build-essential curl wget gnupg python3-dev pybind11-dev - -# Install ODBC driver -curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -dpkg -i packages-microsoft-prod.deb || true -apt-get update -ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 unixodbc-dev - -# Setup Python environment -python3 -m venv /opt/venv -source /opt/venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt - -# Build C++ extensions -cd mssql_python/pybind -chmod +x build.sh -./build.sh -cd /workspace - -# Run tests with localhost SQL Server -export DB_CONNECTION_STRING="Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;" -python -m pytest -v --tb=short --cache-clear -""" - - result = await container.with_exec(["bash", "-c", pipeline_script]).sync() - - console.print( - Panel.fit( - "[bold green]๐ŸŽ‰ All tests completed successfully![/bold green]", - border_style="green", - title="Success", - ) - ) - return True - - except dagger.ExecError as e: - console.print(f"[red]โŒ Pipeline failed[/red]") - console.print(f"[yellow]STDOUT:[/yellow]\n{e.stdout}") - console.print(f"[red]STDERR:[/red]\n{e.stderr}") - return False - except Exception as e: - console.print(f"[red]โŒ Unexpected error: {e}[/red]") - return False - - def clean_cache(self): - """Placeholder for cleaning caches (Dagger manages volumes internally).""" - console.print("๐Ÿงน Cache cleanup not implemented โ€” use `dagger engine reset` if needed.") \ No newline at end of file diff --git a/dockybot/dagger_client_robust.py b/dockybot/dagger_client_robust.py deleted file mode 100644 index a68514d7..00000000 --- a/dockybot/dagger_client_robust.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -DockyBot Dagger Client - Ubuntu test pipeline - -Robust implementation with proper timeout handling. -""" - -import asyncio -import signal -import sys -import dagger -from rich.console import Console -from rich.panel import Panel - -console = Console() - - -class DaggerClient: - """Dagger client for running Ubuntu test pipeline.""" - - def __init__(self, verbose: bool = False, cache_enabled: bool = True): - self.verbose = verbose - self.cache_enabled = cache_enabled - - def run_tests(self, platform: str = "ubuntu") -> bool: - """Entry point for running tests on a given platform (only ubuntu supported).""" - if platform != "ubuntu": - raise ValueError("Only Ubuntu platform is supported for now") - - # Set up signal handler for graceful termination - def signal_handler(signum, frame): - console.print("\n[yellow]โš ๏ธ Received interrupt signal. Cleaning up...[/yellow]") - sys.exit(1) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - return asyncio.run(asyncio.wait_for(self._run_tests_async(), timeout=1800)) # 30 min timeout - except asyncio.TimeoutError: - console.print("[red]โŒ Pipeline timed out after 30 minutes[/red]") - return False - except KeyboardInterrupt: - console.print("\n[yellow]โš ๏ธ Operation cancelled by user[/yellow]") - return False - - async def _run_tests_async(self) -> bool: - """Run the Ubuntu PR pipeline inside Dagger with localhost SQL Server.""" - - # Simple non-hanging approach - no persistent connections - try: - console.print("[bold green]๐Ÿณ Starting Dagger engine...[/bold green]") - - # Create connection with shorter timeout - config = dagger.Config(log_output=sys.stderr, timeout=600) # 10 min timeout - - async with dagger.Connection(config) as client: - # Workspace - workspace = client.host().directory( - ".", - exclude=[".git/", "__pycache__/", "*.pyc", "venv/", "build/", "dist/"], - ) - - # Create and run container in one shot - console.print("๐Ÿง Running Ubuntu pipeline...") - - # Simplified pipeline script - pipeline_script = ''' -#!/bin/bash -set -euo pipefail - -echo "๐Ÿ”ง Installing system dependencies..." -export DEBIAN_FRONTEND=noninteractive -apt-get update -qq -apt-get install -y -qq python3 python3-pip python3-venv cmake build-essential curl wget gnupg python3-dev - -echo "๐Ÿ“€ Installing ODBC driver..." -curl -sSL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - -curl -sSL https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list -apt-get update -qq -ACCEPT_EULA=Y apt-get install -y -qq msodbcsql18 mssql-tools18 unixodbc-dev - -echo "๐Ÿ Setting up Python environment..." -python3 -m venv /opt/venv -source /opt/venv/bin/activate -pip install --upgrade pip -q -pip install -r requirements.txt -q - -echo "๐Ÿ”จ Building C++ extensions..." -cd mssql_python/pybind -chmod +x build.sh -./build.sh -cd /workspace - -echo "๐Ÿงช Running tests..." -export DB_CONNECTION_STRING="Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;" -python -m pytest tests/ -v --tb=short --maxfail=5 -x - -echo "โœ… Pipeline completed successfully!" -''' - - # Execute with timeout - result = await asyncio.wait_for( - client.container() - .from_("ubuntu:22.04") - .with_workdir("/workspace") - .with_directory("/workspace", workspace) - .with_exec(["bash", "-c", pipeline_script]) - .sync(), - timeout=1200 # 20 minute timeout for the container execution - ) - - console.print( - Panel.fit( - "[bold green]๐ŸŽ‰ All tests completed successfully![/bold green]", - border_style="green", - title="Success", - ) - ) - return True - - except asyncio.TimeoutError: - console.print("[red]โŒ Container execution timed out[/red]") - return False - except dagger.ExecError as e: - console.print(f"[red]โŒ Pipeline failed[/red]") - if self.verbose and e.stdout: - console.print(f"[yellow]STDOUT:[/yellow]\n{e.stdout[:2000]}...") - if e.stderr: - console.print(f"[red]STDERR:[/red]\n{e.stderr[:1000]}...") - return False - except Exception as e: - console.print(f"[red]โŒ Unexpected error: {e}[/red]") - return False - - def clean_cache(self): - """Clean Dagger cache.""" - console.print("๐Ÿงน To clean Dagger cache, run: dagger engine reset") \ No newline at end of file From bf578a3638d1e7665bf31cdae338b4480f059ac1 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Sep 2025 15:17:20 +0530 Subject: [PATCH 3/4] Refactored and added bash --- dockybot/README.md | 355 ++++++++++++++++++++++++++++++++ dockybot/cli.py | 127 +++--------- dockybot/docker_runner.py | 421 +++++++++++++++++++++++++++++++++++++- 3 files changed, 798 insertions(+), 105 deletions(-) create mode 100644 dockybot/README.md diff --git a/dockybot/README.md b/dockybot/README.md new file mode 100644 index 00000000..208d03c5 --- /dev/null +++ b/dockybot/README.md @@ -0,0 +1,355 @@ +# ๐Ÿค– DockyBot + +
+ +![DockyBot](https://img.shields.io/badge/DockyBot-๐Ÿณ_Powered-blue?style=for-the-badge) +![Python](https://img.shields.io/badge/Python-3.10+-brightgreen?style=for-the-badge&logo=python) +![Docker](https://img.shields.io/badge/Docker-Required-2496ED?style=for-the-badge&logo=docker) +![Platform](https://img.shields.io/badge/Platforms-4_Supported-orange?style=for-the-badge) + +**๐Ÿš€ DevOps-as-Code testing tool using Docker for cross-platform testing** + +*Test your mssql-python library across multiple Linux distributions with beautiful, interactive output* + +
+ +--- + +## ๐ŸŒŸ Features + +### ๐Ÿงช **Automated Testing** +- ๐Ÿณ **Multi-Platform Support**: Ubuntu, Alpine, CentOS, Debian +- ๐Ÿ“Š **Beautiful Progress Bars**: Real-time progress tracking with Rich UI +- ๐ŸŽจ **Colored Output**: Smart log formatting with emoji indicators +- โšก **Smart Caching**: Build once, run instantly + +### ๐Ÿ› ๏ธ **Interactive Development** +- ๐Ÿ–ฅ๏ธ **Bash Sessions**: Drop into fully-configured containers +- ๐Ÿ“ฆ **Pre-installed Dependencies**: System packages, ODBC drivers, Python libs +- ๐Ÿ”— **Database Ready**: Pre-configured SQL Server connection +- ๐Ÿ’พ **Persistent Images**: No rebuild needed between sessions + +### ๐Ÿ“ˆ **Advanced Monitoring** +- ๐Ÿ“Š **Step-by-Step Progress**: Track installation phases +- โฑ๏ธ **Elapsed Time Tracking**: See exactly how long each phase takes +- ๐ŸŽฏ **Smart Status Detection**: Automatically detects build phases +- ๐Ÿ“ **Detailed Summaries**: Complete test reports with metrics + +--- + +## ๐Ÿš€ Quick Start + +### Prerequisites +- ๐Ÿณ **Docker** (with daemon running) +- ๐Ÿ **Python 3.10+** +- ๐Ÿ“ฆ **Dependencies**: `pip install docker typer rich` + +### Installation +```bash +# Navigate to mssql-python directory +cd /path/to/mssql-python + +# Install dependencies (if not already installed) +pip install docker typer rich + +# You're ready to go! ๐ŸŽ‰ +``` + +--- + +## ๐Ÿ“– Usage Guide + +### ๐Ÿงช **Running Tests** + +#### Test Single Platform +```bash +# Run tests on Ubuntu (recommended) +python -m dockybot test ubuntu + +# Run with verbose output +python -m dockybot test ubuntu -v + +# Test other platforms +python -m dockybot test alpine +python -m dockybot test centos +python -m dockybot test debian +``` + +#### Test All Platforms +```bash +# Run tests across all supported platforms +python -m dockybot test-all + +# With verbose output +python -m dockybot test-all -v +``` + +### ๐Ÿ–ฅ๏ธ **Interactive Development** + +#### Bash Into Container +```bash +# Open interactive bash session (Ubuntu) +python -m dockybot bash ubuntu + +# With verbose setup output +python -m dockybot bash ubuntu -v +``` + +**What you get in the bash session:** +- โœ… **All system dependencies** installed +- โœ… **Microsoft ODBC Driver 18** configured +- โœ… **Python virtual environment** activated +- โœ… **All requirements.txt** packages installed +- โœ… **C++ extensions** built and ready +- โœ… **Database connection** pre-configured +- โœ… **Workspace mounted** at `/workspace` + +#### Manual Testing Inside Container +```bash +# Inside the container, you can: +python -m pytest tests/ -v # Run all tests +python -m pytest tests/test_003* -v # Run specific test +python main.py # Run your scripts +pip list # Check installed packages +echo $DB_CONNECTION_STRING # Check DB config +``` + +### ๐Ÿ–ผ๏ธ **Image Management** + +#### List Cached Images +```bash +# See all DockyBot cached images +python -m dockybot images +``` + +#### Clean Cached Images +```bash +# Clean specific platform +python -m dockybot clean ubuntu + +# Clean all cached images +python -m dockybot clean + +# Force clean without confirmation +python -m dockybot clean --force +``` + +### ๐Ÿ“‹ **Platform Information** +```bash +# List all supported platforms +python -m dockybot platforms +``` + +--- + +## ๐ŸŽจ Output Examples + +### ๐Ÿงช **Test Output** +``` +๐Ÿš€ Starting DockyBot Test Suite +๐Ÿณ Platform: Ubuntu +โฐ Starting at: UTC time +๐Ÿ”— Database: SQL Server via host.docker.internal + +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿณ Running Ubuntu Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +๐Ÿ”„ Installing system dependencies โ”โ”โ”โ”โ”โ”โ”โ”โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 25% 0:01:23 + +๐Ÿ“ฆ Setting up tzdata (2025b-0ubuntu0.22.04.1) ... +โœ… Successfully installed docker-7.1.0 typer-0.19.1 rich-14.1.0 +๐Ÿงช tests/test_003_connection.py::test_connection_close PASSED [45%] +โœ… tests/test_007_logging.py::test_setup_logging PASSED [89%] + +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Test Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿค– DockyBot Results โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ Platform: Ubuntu โ”‚ +โ”‚ Status: โœ… PASSED โ”‚ +โ”‚ Duration: 2:34 โ”‚ +โ”‚ Log Lines: 1247 โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +``` + +### ๐Ÿ–ฅ๏ธ **Interactive Session** +``` +๐Ÿš€ Starting DockyBot Interactive Session +๐Ÿณ Platform: Ubuntu +๐Ÿ–ผ๏ธ Image: dockybot/ubuntu:latest + +๐ŸŽฏ Container ready! Attaching to interactive session... +๐Ÿ“ Type 'exit' to leave the container + +root@container:/workspace# python -m pytest tests/ -v +root@container:/workspace# pip list +root@container:/workspace# python main.py +``` + +--- + +## ๐Ÿ—๏ธ Architecture + +### ๐Ÿ“‚ **File Structure** +``` +dockybot/ +โ”œโ”€โ”€ README.md # ๐Ÿ“– This documentation +โ”œโ”€โ”€ __init__.py # ๐Ÿ“ฆ Package initialization +โ”œโ”€โ”€ __main__.py # ๐Ÿš€ Entry point +โ”œโ”€โ”€ cli.py # ๐Ÿ–ฅ๏ธ Command-line interface +โ”œโ”€โ”€ docker_runner.py # ๐Ÿณ Docker abstraction layer +โ”œโ”€โ”€ platforms.py # ๐ŸŒ Platform configurations +โ””โ”€โ”€ scripts/ # ๐Ÿ“œ Platform-specific setup scripts + โ”œโ”€โ”€ ubuntu.sh # ๐ŸŸ  Ubuntu 22.04 setup + โ”œโ”€โ”€ alpine.sh # ๐Ÿ”ต Alpine 3.18 setup + โ”œโ”€โ”€ centos.sh # ๐Ÿ”ด CentOS 7 setup + โ””โ”€โ”€ debian.sh # ๐ŸŸฃ Debian 11 setup +``` + +### ๐Ÿณ **Docker Strategy** + +#### **Smart Image Caching** +- **First Run**: Builds `dockybot/ubuntu:latest` with all dependencies +- **Subsequent Runs**: Reuses cached image for instant startup +- **Containers**: Removed after each session (`--rm`) +- **Images**: Persist until manually cleaned + +#### **Platform Configurations** +| Platform | Base Image | Package Manager | Shell | +|----------|------------|----------------|-------| +| ๐ŸŸ  Ubuntu | `ubuntu:22.04` | `apt` | `bash` | +| ๐Ÿ”ต Alpine | `alpine:3.18` | `apk` | `sh` | +| ๐Ÿ”ด CentOS | `centos:7` | `yum` | `bash` | +| ๐ŸŸฃ Debian | `debian:11` | `apt` | `bash` | + +--- + +## ๐ŸŽฏ Advanced Usage + +### ๐Ÿ”ง **Custom Environment Variables** +```bash +# Override default database connection +export DB_CONNECTION_STRING="your_custom_connection_string" +python -m dockybot test ubuntu +``` + +### ๐Ÿณ **Direct Docker Commands** +```bash +# Check DockyBot images +docker images | grep dockybot + +# Run container manually +docker run -it --rm -v $(pwd):/workspace dockybot/ubuntu:latest bash + +# Clean up everything +docker system prune -a +``` + +### ๐Ÿ“Š **Debugging & Logs** +```bash +# Enable verbose output for detailed logging +python -m dockybot test ubuntu -v + +# Check Docker daemon logs +docker system events + +# Container resource usage +docker stats +``` + +--- + +## ๐Ÿƒโ€โ™‚๏ธ Performance Tips + +### โšก **Speed Optimizations** +- **Use cached images**: First run takes 3-5 minutes, subsequent runs are instant +- **Keep images**: Only clean when needed (`python -m dockybot clean`) +- **Use Ubuntu**: Fastest platform for most testing scenarios +- **Verbose mode**: Only use `-v` when debugging + +### ๐Ÿ’พ **Storage Management** +```bash +# Check image sizes +python -m dockybot images + +# Clean specific platforms you don't use +python -m dockybot clean alpine centos debian + +# Full cleanup (forces rebuild next time) +docker system prune -a +``` + +--- + +## ๐Ÿ› Troubleshooting + +### Common Issues + +#### ๐Ÿณ **Docker Not Running** +```bash +# Error: Cannot connect to the Docker daemon +# Solution: Start Docker Desktop or daemon +systemctl start docker # Linux +open -a Docker # macOS +``` + +#### ๐Ÿ”’ **Permission Issues** +```bash +# Error: Permission denied accessing Docker +# Solution: Add user to docker group (Linux) +sudo usermod -aG docker $USER +# Then logout and login again +``` + +#### ๐Ÿ’พ **Disk Space** +```bash +# Error: No space left on device +# Solution: Clean Docker system +docker system prune -a +python -m dockybot clean --force +``` + +#### ๐ŸŒ **Network Issues** +```bash +# Error: Cannot connect to SQL Server +# Check: host.docker.internal resolves correctly +docker run --rm alpine ping -c 1 host.docker.internal +``` + +### ๐Ÿ“ž **Getting Help** +- ๐Ÿ“– **Documentation**: Check this README +- ๐Ÿ› **Issues**: Report bugs in the main repository +- ๐Ÿ’ฌ **Discussions**: Use GitHub Discussions for questions +- ๐Ÿ“ง **Contact**: Reach out to the mssql-python team + +--- + +## ๐Ÿ“ˆ Roadmap + +### ๐Ÿš€ **Coming Soon** +- ๐ŸŽ **macOS Support**: ARM64 and Intel testing +- ๐ŸชŸ **Windows Containers**: Native Windows testing +- ๐Ÿงช **Test Parallelization**: Run multiple platforms simultaneously +- ๐Ÿ“Š **HTML Reports**: Beautiful test result dashboards +- ๐Ÿ”„ **CI Integration**: GitHub Actions workflows +- ๐Ÿ **Python Version Matrix**: Test across Python versions + +### ๐Ÿ’ก **Ideas & Suggestions** +We're always looking for ways to improve DockyBot! Feel free to: +- ๐ŸŒŸ Star the repository +- ๐Ÿ› Report issues +- ๐Ÿ’ก Suggest features +- ๐Ÿค Contribute code + +--- + +
+ +## ๐ŸŽ‰ Happy Testing with DockyBot! + +**Made with โค๏ธ by the mssql-python team** + +*Empowering developers to test across platforms effortlessly* ๐Ÿš€ + +--- + +![Footer](https://img.shields.io/badge/๐Ÿค–_DockyBot-Powered_by_Docker-blue?style=for-the-badge) + +
\ No newline at end of file diff --git a/dockybot/cli.py b/dockybot/cli.py index d15c86ef..0099d878 100644 --- a/dockybot/cli.py +++ b/dockybot/cli.py @@ -29,6 +29,38 @@ def test( if not success: raise typer.Exit(1) +@app.command() +def bash( + platform: str = typer.Argument(..., help="Platform to bash into"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output") +): + """Open interactive bash session in platform container with dependencies installed.""" + if platform not in PLATFORMS: + console.print(f"[red]Error:[/red] Unknown platform '{platform}'") + console.print(f"Available platforms: {', '.join(PLATFORMS.keys())}") + raise typer.Exit(1) + + runner = DockerRunner() + success = runner.run_platform_bash(platform, verbose=verbose) + + if not success: + raise typer.Exit(1) + +@app.command() +def images(): + """List DockyBot cached images.""" + runner = DockerRunner() + runner.list_cached_images() + +@app.command() +def clean( + platform: str = typer.Argument(None, help="Platform to clean (optional - cleans all if not specified)"), + force: bool = typer.Option(False, "--force", "-f", help="Force removal without confirmation") +): + """Clean DockyBot cached images.""" + runner = DockerRunner() + runner.clean_cached_images(platform, force) + @app.command() def platforms(): """List available platforms.""" @@ -69,98 +101,3 @@ def test_all( if __name__ == "__main__": app() - -import typer -from typing import Optional -from rich.console import Console -from rich.panel import Panel -from pathlib import Path - -from .docker_client import DockerClient - -app = typer.Typer( - name="dockybot", - help="๐Ÿค– DockyBot - DevOps-as-Code testing tool using Docker", - no_args_is_help=True, - rich_markup_mode="rich" -) - -console = Console() - - -@app.command() -def test( - cache: bool = typer.Option( - True, - "--cache/--no-cache", - help="Enable/disable pip and build artifact caching" - ), - verbose: bool = typer.Option( - False, - "--verbose", - "-v", - help="Enable verbose output" - ) -): - """ - Run tests on Ubuntu using Docker (replicates exact PR pipeline steps). - """ - console.print(Panel.fit( - f"๐Ÿš€ [bold blue]DockyBot Ubuntu Test Runner[/bold blue]\n" - f"Platform: Ubuntu 22.04\n" - f"Caching: {'โœ… Enabled' if cache else 'โŒ Disabled'}", - border_style="blue" - )) - - console.print("๐Ÿ”„ [bold yellow]Testing on Ubuntu...[/bold yellow]") - - # Create Docker client and run tests - try: - client = DockerClient(verbose=verbose, cache_enabled=cache) - success = client.run_tests(platform="ubuntu") - - if success: - console.print("[bold green]โœ… All tests passed![/bold green]") - else: - console.print("[bold red]โŒ Tests failed![/bold red]") - raise typer.Exit(1) - - except Exception as e: - console.print(f"[red]โŒ Error: {e}[/red]") - raise typer.Exit(1) - - -@app.command() -def clean(): - """ - Clean up Docker cache and containers. - """ - console.print("๐Ÿงน [bold yellow]Cleaning Docker cache...[/bold yellow]") - - try: - client = DockerClient() - client.clean_cache() - except Exception as e: - console.print(f"[red]โŒ Error cleaning cache: {e}[/red]") - raise typer.Exit(1) - - -@app.command() -def version(): - """ - Show DockyBot version information. - """ - try: - from . import __version__ - console.print(f"DockyBot v{__version__} (Docker-based)") - except ImportError: - console.print("DockyBot v1.0.0 (Docker-based)") - - -def main(): - """Entry point for the CLI.""" - app() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/dockybot/docker_runner.py b/dockybot/docker_runner.py index b8fe0923..b5d974ce 100644 --- a/dockybot/docker_runner.py +++ b/dockybot/docker_runner.py @@ -75,10 +75,6 @@ def run_platform(self, platform_name: str, script_content: str) -> bool: 'TZ': 'UTC' } - # Override with any environment variables from host if they exist - if 'DB_CONNECTION_STRING' in os.environ: - environment['DB_CONNECTION_STRING'] = os.environ['DB_CONNECTION_STRING'] - # Run container container = self.client.containers.run( platform_config['image'], @@ -273,7 +269,7 @@ def _display_formatted_line(self, line: str) -> None: # Test results (highest priority - check these first) if " passed " in line_lower or line_lower.endswith(" passed"): console.print(f"[green]โœ… {line}[/green]") - elif " failed " in line_lower or line_lower.endswith(" failed"): + elif " failed " in line_lower or line_lower.endswith(" failed") or " error " in line_lower or line_lower.endswith(" failed"): console.print(f"[red]โŒ {line}[/red]") elif " skipped " in line_lower or line_lower.endswith(" skipped"): console.print(f"[yellow]โš ๏ธ {line}[/yellow]") @@ -285,16 +281,16 @@ def _display_formatted_line(self, line: str) -> None: # Installation success messages (check before error patterns) elif any(phrase in line_lower for phrase in [ 'successfully installed', 'successfully uninstalled', 'successfully', - 'setting up', 'processing triggers', 'created symlink', - 'collected packages', 'downloading', 'requirement already satisfied' + 'created symlink', + 'collected packages', 'requirement already satisfied' ]): console.print(f"[green]โœ… {line}[/green]") # Package installation messages elif any(phrase in line_lower for phrase in [ 'installing collected packages', 'collecting', 'unpacking', 'preparing to unpack', - 'selecting previously unselected', 'installing system dependencies', - 'installing microsoft odbc', 'installing python packages' + 'setting up', 'selecting previously unselected', 'installing system dependencies', + 'installing microsoft odbc', 'installing python packages', 'downloading', 'processing triggers' ]): console.print(f"[cyan]๐Ÿ“ฆ {line}[/cyan]") @@ -405,6 +401,411 @@ def run_platform_test(self, platform: str, verbose: bool = False) -> bool: console.print(f"[red]๐Ÿ’ฅ Error running {platform} test: {e}[/red]") return False + def run_platform_bash(self, platform: str, verbose: bool = False) -> bool: + """Run interactive bash session in platform container with dependencies installed.""" + self.verbose = verbose + + try: + # Check if we have a cached image, if not build it + image_name = f"dockybot/{platform}:latest" + + if not self._image_exists(image_name): + console.print(f"๐Ÿ—๏ธ [bold yellow]Building cached image for {platform}...[/bold yellow]") + console.print("โš™๏ธ [dim]This will take a few minutes but only happens once![/dim]") + console.print(f"๐Ÿ’พ [dim]Image will be saved as: {image_name}[/dim]") + + # Build the image with all dependencies + success = self._build_platform_image(platform, image_name) + if not success: + return False + + console.print(f"โœ… [bold green]Image built and cached![/bold green]") + console.print(f"๐Ÿ” [dim]You can see it with: docker images | grep dockybot[/dim]") + else: + console.print(f"๐Ÿš€ [bold green]Using cached image:[/bold green] {image_name}") + console.print(f"โšก [dim]No rebuild needed - dependencies already installed![/dim]") + + # Run interactive session using the cached image + return self._run_interactive_session(platform, image_name) + + except Exception as e: + console.print(f"[red]๐Ÿ’ฅ Error running {platform} bash session: {e}[/red]") + return False + + def _image_exists(self, image_name: str) -> bool: + """Check if Docker image exists locally.""" + try: + self.client.images.get(image_name) + return True + except: + return False + + def _build_platform_image(self, platform: str, image_name: str) -> bool: + """Build a cached Docker image with all dependencies installed.""" + try: + # Get the setup script + script_content = get_platform_script(platform) + setup_script = self._create_build_script(script_content) + + # Create Dockerfile + dockerfile_content = self._create_dockerfile(platform, setup_script) + + # Build image + console.print(f"๐Ÿ“ฆ [cyan]Building image {image_name}...[/cyan]") + + # Create build context + import tempfile + import tarfile + import io + + # Create tar archive with Dockerfile and setup script + tar_buffer = io.BytesIO() + with tarfile.open(fileobj=tar_buffer, mode='w') as tar: + # Add Dockerfile + dockerfile_info = tarfile.TarInfo(name='Dockerfile') + dockerfile_info.size = len(dockerfile_content.encode()) + tar.addfile(dockerfile_info, io.BytesIO(dockerfile_content.encode())) + + # Add setup script + script_info = tarfile.TarInfo(name='setup.sh') + script_info.size = len(setup_script.encode()) + script_info.mode = 0o755 + tar.addfile(script_info, io.BytesIO(setup_script.encode())) + + tar_buffer.seek(0) + + # Build the image + self.client.images.build( + fileobj=tar_buffer, + tag=image_name, + custom_context=True, + rm=True + ) + + console.print(f"โœ… [bold green]Successfully built image:[/bold green] {image_name}") + return True + + except Exception as e: + console.print(f"[red]โŒ Error building image: {e}[/red]") + return False + + def _create_dockerfile(self, platform: str, setup_script: str) -> str: + """Create Dockerfile for the platform.""" + platform_config = self._get_platform_config(platform) + base_image = platform_config['image'] + + dockerfile = f"""FROM {base_image} + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=UTC +ENV PYTHONPATH=/workspace +ENV DB_CONNECTION_STRING="Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;" + +# Copy and run setup script +COPY setup.sh /tmp/setup.sh +RUN chmod +x /tmp/setup.sh && /tmp/setup.sh + +# Set working directory +WORKDIR /workspace + +# Default command +CMD ["bash"] +""" + return dockerfile + + def _create_build_script(self, original_script: str) -> str: + """Create build script that installs dependencies but doesn't run tests.""" + lines = original_script.split('\n') + build_lines = [] + + for line in lines: + # Skip the shebang and set -euo pipefail for build + if line.startswith('#!') or 'set -euo pipefail' in line: + continue + # Stop before running tests + if any(test_indicator in line.lower() for test_indicator in + ['python -m pytest', 'pytest', 'running tests']): + break + build_lines.append(line) + + # Add final steps for image + build_lines.extend([ + '', + 'echo "๐ŸŽ‰ DockyBot image build complete!"', + 'echo "โœ… All dependencies installed and ready to use"' + ]) + + return '\n'.join(build_lines) + + def _run_interactive_session(self, platform: str, image_name: str) -> bool: + """Run interactive bash session using cached image.""" + + console.print(f"\n๐Ÿš€ [bold green]Starting DockyBot Interactive Session[/bold green]") + console.print(f"๐Ÿณ [bold blue]Platform:[/bold blue] {platform.title()}") + console.print(f"๐Ÿ–ผ๏ธ [bold cyan]Image:[/bold cyan] {image_name}") + console.print() + + try: + # Create or reuse named container + container_name = f"dockybot-{platform}-session" + + try: + # Try to remove existing container if it exists + existing = self.client.containers.get(container_name) + existing.remove(force=True) + except: + pass # Container doesn't exist, which is fine + + console.print(f"๐ŸŽฏ [bold green]Starting container...[/bold green]") + console.print(f"๐Ÿ“ [dim]Type 'exit' to leave the container[/dim]") + console.print() + + # Run interactive container + import subprocess + import warnings + + # Suppress urllib3 warnings that occur during container cleanup + warnings.filterwarnings("ignore", category=ResourceWarning) + + try: + result = subprocess.run([ + 'docker', 'run', '--rm', '-it', + '--name', container_name, + '-v', f'{str(self.workspace)}:/workspace', + '-w', '/workspace', + '--add-host', 'host.docker.internal:host-gateway', + '-e', 'DB_CONNECTION_STRING=Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;', + image_name, + 'bash', '-c', 'source /opt/venv/bin/activate 2>/dev/null || true; exec bash' + ], cwd=str(self.workspace)) + + console.print(f"\n๐Ÿ‘‹ [dim]Session ended. Container cleaned up.[/dim]") + return result.returncode == 0 + + except KeyboardInterrupt: + console.print(f"\nโš ๏ธ [yellow]Session interrupted. Cleaning up...[/yellow]") + # Try to stop the container gracefully + try: + running_container = self.client.containers.get(container_name) + running_container.stop(timeout=5) + except: + pass + return False + + except Exception as e: + console.print(f"[red]๐Ÿ’ฅ Error running interactive session: {e}[/red]") + return False + + def _create_setup_script(self, original_script: str) -> str: + """Create setup script that stops before running tests and keeps container alive.""" + lines = original_script.split('\n') + setup_lines = [] + + for line in lines: + # Stop before running tests + if any(test_indicator in line.lower() for test_indicator in + ['python -m pytest', 'pytest', 'running tests']): + break + setup_lines.append(line) + + # Add keeping container alive instead of exec bash + setup_lines.extend([ + '', + 'echo "๐ŸŽ‰ Setup complete! Container ready for interactive session..."', + 'echo "๐Ÿ’ก You can now run tests manually with: python -m pytest tests/ -v"', + 'echo "๐Ÿ“ Workspace is mounted at: /workspace"', + 'echo "๐Ÿ Python environment is activated"', + 'echo "๐Ÿ”— DB_CONNECTION_STRING is configured"', + 'echo ""', + 'cd /workspace', + 'source /opt/venv/bin/activate', + '# Keep container running', + 'tail -f /dev/null' + ]) + + return '\n'.join(setup_lines) + + def run_platform_interactive(self, platform_name: str, script_content: str) -> bool: + """Run platform in interactive mode.""" + + # Print welcome message + console.print(f"\n๐Ÿš€ [bold green]Starting DockyBot Interactive Session[/bold green]") + console.print(f"๐Ÿณ [bold blue]Platform:[/bold blue] {platform_name.title()}") + console.print(f"๐Ÿ”ง [bold yellow]Mode:[/bold yellow] Interactive Bash") + console.print() + + # Create temp script + with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f: + f.write(script_content) + script_path = f.name + + try: + os.chmod(script_path, 0o755) + + # Get platform config + platform_config = self._get_platform_config(platform_name) + + # Prepare environment variables + environment = { + 'DB_CONNECTION_STRING': 'Driver=ODBC Driver 18 for SQL Server;Server=host.docker.internal,1433;Database=master;UID=sa;Pwd=Str0ng@Passw0rd123;Encrypt=no;TrustServerCertificate=yes;', + 'PYTHONPATH': '/workspace', + 'DEBIAN_FRONTEND': 'noninteractive', + 'TZ': 'UTC' + } + + # Create or reuse named container + container_name = f"dockybot-{platform_name}" + + console.print(f"๐Ÿ—๏ธ [bold cyan]Setting up container:[/bold cyan] {container_name}") + console.print("โš™๏ธ [dim]Installing dependencies... This may take a few minutes on first run.[/dim]") + console.print() + + try: + # Try to remove existing container if it exists + existing = self.client.containers.get(container_name) + existing.remove(force=True) + except: + pass # Container doesn't exist, which is fine + + # Run container interactively + container = self.client.containers.run( + platform_config['image'], + command=platform_config['command'] + [f"/tmp/setup.sh"], + name=container_name, + volumes={ + str(self.workspace): {"bind": "/workspace", "mode": "rw"}, + script_path: {"bind": "/tmp/setup.sh", "mode": "ro"} + }, + working_dir="/workspace", + environment=environment, + detach=True, + **platform_config.get('docker_options', {}) + ) + + # Wait for setup to complete + console.print("โณ [yellow]Waiting for setup to complete...[/yellow]") + + # Stream setup logs to show progress + for log_line in container.logs(stream=True, follow=True): + line = log_line.decode('utf-8').strip() + if line: + self._display_formatted_line(line) + # Break when we see the completion message + if "Setup complete! Container ready for interactive session" in line: + break + + console.print(f"๐ŸŽฏ [bold green]Container ready![/bold green] Attaching to interactive session...") + console.print(f"๐Ÿ“ [dim]Type 'exit' to leave the container[/dim]") + console.print() + + # Attach to container for interactive session with proper environment + import subprocess + result = subprocess.run([ + 'docker', 'exec', '-it', '-e', 'DB_CONNECTION_STRING=' + environment['DB_CONNECTION_STRING'], + container_name, 'bash', '-c', + 'cd /workspace && source /opt/venv/bin/activate && exec bash' + ], cwd=str(self.workspace)) + + # Clean up + try: + container.remove(force=True) + except: + pass + + return result.returncode == 0 + + finally: + if os.path.exists(script_path): + os.unlink(script_path) + def list_platforms(self) -> list: """List available platforms.""" - return ['ubuntu', 'alpine', 'centos', 'debian'] \ No newline at end of file + return ['ubuntu', 'alpine', 'centos', 'debian'] + + def list_cached_images(self) -> None: + """List DockyBot cached images.""" + console.print("\n๐Ÿ–ผ๏ธ [bold blue]DockyBot Cached Images[/bold blue]") + console.print() + + images = self.client.images.list() + dockybot_images = [img for img in images if any('dockybot/' in tag for tag in img.tags)] + + if not dockybot_images: + console.print("๐Ÿ“ญ [dim]No cached images found[/dim]") + console.print("๐Ÿ’ก [dim]Run 'python -m dockybot bash ' to create one[/dim]") + return + + from rich.table import Table + table = Table() + table.add_column("Image", style="cyan") + table.add_column("Size", style="green") + table.add_column("Created", style="yellow") + + for image in dockybot_images: + for tag in image.tags: + if 'dockybot/' in tag: + size_mb = round(image.attrs['Size'] / (1024 * 1024), 1) + created = image.attrs['Created'][:19].replace('T', ' ') + table.add_row(tag, f"{size_mb} MB", created) + + console.print(table) + console.print() + console.print("๐Ÿ’ก [dim]Use 'python -m dockybot clean' to remove cached images[/dim]") + + def clean_cached_images(self, platform: str = None, force: bool = False) -> None: + """Clean DockyBot cached images.""" + if platform: + image_name = f"dockybot/{platform}:latest" + if not self._image_exists(image_name): + console.print(f"๐Ÿ“ญ [yellow]No cached image found for {platform}[/yellow]") + return + + if not force: + import typer + confirm = typer.confirm(f"Remove cached image for {platform}?") + if not confirm: + console.print("โŒ [dim]Cancelled[/dim]") + return + + try: + self.client.images.remove(image_name, force=True) + console.print(f"๐Ÿ—‘๏ธ [green]Removed cached image: {image_name}[/green]") + except Exception as e: + console.print(f"[red]Error removing image: {e}[/red]") + else: + # Clean all dockybot images + images = self.client.images.list() + dockybot_images = [] + for img in images: + for tag in img.tags: + if 'dockybot/' in tag: + dockybot_images.append(tag) + break + + if not dockybot_images: + console.print("๐Ÿ“ญ [dim]No cached images to clean[/dim]") + return + + if not force: + console.print(f"Found {len(dockybot_images)} cached images:") + for img in dockybot_images: + console.print(f" - {img}") + console.print() + + import typer + confirm = typer.confirm("Remove all DockyBot cached images?") + if not confirm: + console.print("โŒ [dim]Cancelled[/dim]") + return + + removed_count = 0 + for img_tag in dockybot_images: + try: + self.client.images.remove(img_tag, force=True) + console.print(f"๐Ÿ—‘๏ธ [green]Removed: {img_tag}[/green]") + removed_count += 1 + except Exception as e: + console.print(f"[red]Error removing {img_tag}: {e}[/red]") + + console.print(f"\nโœ… [green]Cleaned {removed_count} cached images[/green]") \ No newline at end of file From 4ec566b755a53e781a49b3c79e4e8f82809637eb Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Sep 2025 15:33:25 +0530 Subject: [PATCH 4/4] Delete irrelevant stuff and add a meaningful description --- dockybot/README.md | 6 +++--- dockybot_setup.py | 47 ---------------------------------------------- 2 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 dockybot_setup.py diff --git a/dockybot/README.md b/dockybot/README.md index 208d03c5..9ec55408 100644 --- a/dockybot/README.md +++ b/dockybot/README.md @@ -1,15 +1,15 @@ # ๐Ÿค– DockyBot -
+
![DockyBot](https://img.shields.io/badge/DockyBot-๐Ÿณ_Powered-blue?style=for-the-badge) ![Python](https://img.shields.io/badge/Python-3.10+-brightgreen?style=for-the-badge&logo=python) ![Docker](https://img.shields.io/badge/Docker-Required-2496ED?style=for-the-badge&logo=docker) ![Platform](https://img.shields.io/badge/Platforms-4_Supported-orange?style=for-the-badge) -**๐Ÿš€ DevOps-as-Code testing tool using Docker for cross-platform testing** +**๐Ÿณ Docker-powered cross-platform orchestrator** -*Test your mssql-python library across multiple Linux distributions with beautiful, interactive output* +*Test your library across multiple Linux distributions with beautiful, interactive output*
diff --git a/dockybot_setup.py b/dockybot_setup.py deleted file mode 100644 index 9468883f..00000000 --- a/dockybot_setup.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -""" -Setup script for DockyBot package -""" - -from setuptools import setup, find_packages -from pathlib import Path - -# Read README for long description -readme_path = Path(__file__).parent / "dockybot" / "README.md" -long_description = "" -if readme_path.exists(): - long_description = readme_path.read_text(encoding="utf-8") - -setup( - name="dockybot", - version="0.1.0", - description="DevOps-as-Code CLI for cross-platform testing with Dagger", - long_description=long_description, - long_description_content_type="text/markdown", - author="Microsoft", - packages=find_packages(), - python_requires=">=3.8", - install_requires=[ - "typer[all]>=0.9.0", - "dagger-io>=0.9.0", - "rich>=13.0.0", - ], - entry_points={ - "console_scripts": [ - "dockybot=dockybot.cli:main", - ], - }, - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: Testing", - "Topic :: System :: Systems Administration", - ], -) \ No newline at end of file