validate_docker.py•12.6 kB
#!/usr/bin/env python3
"""
Docker Configuration Validator for Kotlin MCP Server
Validates Docker setup files and provides recommendations
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Tuple
class DockerValidator:
    """Validates Docker configuration and setup"""
    def __init__(self) -> None:
        self.project_root = Path(__file__).parent
        self.issues: List[str] = []
        self.warnings: List[str] = []
        self.recommendations: List[str] = []
    def check_docker_files(self) -> bool:
        """Check if Docker configuration files exist and are valid"""
        print("🐳 Checking Docker configuration files...")
        # Check Dockerfile
        dockerfile_path = self.project_root / "Dockerfile"
        if not dockerfile_path.exists():
            self.issues.append("Dockerfile not found")
            return False
        # Validate Dockerfile content
        dockerfile_content = dockerfile_path.read_text()
        required_elements = [
            "FROM python:",
            "WORKDIR /app",
            "COPY requirements.txt",
            "RUN pip install",
            "COPY . .",
            "CMD",
        ]
        for element in required_elements:
            if element not in dockerfile_content:
                self.issues.append(f"Dockerfile missing: {element}")
        # Check for security best practices
        if "USER mcpuser" not in dockerfile_content:
            self.warnings.append("Dockerfile should use non-root user")
        if "HEALTHCHECK" not in dockerfile_content:
            self.recommendations.append("Add HEALTHCHECK to Dockerfile")
        print("✅ Dockerfile validation complete")
        return True
    def check_docker_compose(self) -> bool:
        """Check docker-compose.yml configuration"""
        print("🔧 Checking Docker Compose configuration...")
        compose_path = self.project_root / "docker-compose.yml"
        if not compose_path.exists():
            self.issues.append("docker-compose.yml not found")
            return False
        try:
            import yaml  # type: ignore
            with open(compose_path) as f:
                compose_config = yaml.safe_load(f)
            # Check required services
            if "services" not in compose_config:
                self.issues.append("No services defined in docker-compose.yml")
                return False
            services = compose_config["services"]
            # Check for main service
            main_service_exists = any("kotlin-mcp" in name for name in services.keys())
            if not main_service_exists:
                self.issues.append("No kotlin-mcp service found in docker-compose.yml")
            # Check for volume mounts
            for service_name, service_config in services.items():
                if "volumes" not in service_config:
                    self.warnings.append(f"Service {service_name} has no volume mounts")
            print("✅ Docker Compose validation complete")
            return True
        except ImportError:
            self.warnings.append("PyYAML not installed - skipping detailed compose validation")
            return True
        except Exception as e:
            self.issues.append(f"Error parsing docker-compose.yml: {e}")
            return False
    def check_dockerignore(self) -> bool:
        """Check .dockerignore file"""
        print("📋 Checking .dockerignore...")
        dockerignore_path = self.project_root / ".dockerignore"
        if not dockerignore_path.exists():
            self.recommendations.append("Create .dockerignore file to optimize builds")
            return True
        content = dockerignore_path.read_text()
        recommended_ignores = ["__pycache__/", ".git/", "*.log", ".venv/", ".pytest_cache/"]
        for ignore_pattern in recommended_ignores:
            if ignore_pattern not in content:
                self.recommendations.append(f"Add '{ignore_pattern}' to .dockerignore")
        print("✅ .dockerignore validation complete")
        return True
    def check_requirements_compatibility(self) -> bool:
        """Check if requirements.txt is compatible with Docker"""
        print("📦 Checking Python requirements compatibility...")
        req_path = self.project_root / "requirements.txt"
        if not req_path.exists():
            self.issues.append("requirements.txt not found")
            return False
        content = req_path.read_text()
        # Check for potentially problematic packages
        problematic_packages = {
            "opencv-python": "Consider using opencv-python-headless for Docker",
            "tkinter": "GUI packages may not work in headless containers",
            "pyqt": "GUI packages may not work in headless containers",
        }
        for package, warning in problematic_packages.items():
            if package in content.lower():
                self.warnings.append(f"{package}: {warning}")
        # Check for version pinning
        lines = [
            line.strip()
            for line in content.split("\n")
            if line.strip() and not line.startswith("#")
        ]
        unpinned = [
            line for line in lines if ">=" not in line and "==" not in line and "~=" not in line
        ]
        if unpinned:
            self.recommendations.append(f"Consider pinning versions for: {', '.join(unpinned[:3])}")
        print("✅ Requirements compatibility check complete")
        return True
    def check_docker_installation(self) -> Tuple[bool, str]:
        """Check if Docker is installed and accessible"""
        print("🔍 Checking Docker installation...")
        try:
            result = subprocess.run(
                ["docker", "--version"], capture_output=True, text=True, timeout=10
            )
            if result.returncode == 0:
                version = result.stdout.strip()
                print(f"✅ Docker found: {version}")
                return True, version
            else:
                return False, "Docker command failed"
        except FileNotFoundError:
            return False, "Docker not installed"
        except subprocess.TimeoutExpired:
            return False, "Docker command timed out"
        except Exception as e:
            return False, f"Error checking Docker: {e}"
    def check_docker_compose_installation(self) -> Tuple[bool, str]:
        """Check if Docker Compose is available"""
        print("🔧 Checking Docker Compose installation...")
        # Try docker-compose command
        try:
            result = subprocess.run(
                ["docker-compose", "--version"], capture_output=True, text=True, timeout=10
            )
            if result.returncode == 0:
                version = result.stdout.strip()
                print(f"✅ Docker Compose found: {version}")
                return True, version
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        # Try docker compose command (newer versions)
        try:
            result = subprocess.run(
                ["docker", "compose", "version"], capture_output=True, text=True, timeout=10
            )
            if result.returncode == 0:
                version = result.stdout.strip()
                print(f"✅ Docker Compose (plugin) found: {version}")
                return True, version
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        return False, "Docker Compose not found"
    def validate_build_context(self) -> bool:
        """Validate Docker build context"""
        print("📁 Validating build context...")
        required_files = ["kotlin_mcp_server.py", "requirements.txt", "pyproject.toml"]
        for file_name in required_files:
            if not (self.project_root / file_name).exists():
                self.issues.append(f"Required file missing: {file_name}")
        # Check for large files that should be ignored
        large_dirs = ["__pycache__", ".git", "htmlcov", ".pytest_cache"]
        for dir_name in large_dirs:
            if (self.project_root / dir_name).exists():
                self.recommendations.append(f"Ensure {dir_name} is in .dockerignore")
        print("✅ Build context validation complete")
        return len(self.issues) == 0
    def generate_setup_commands(self) -> List[str]:
        """Generate Docker setup commands based on platform"""
        commands = []
        # Detect platform
        import platform
        system = platform.system().lower()
        if system == "darwin":  # macOS
            commands.extend(
                [
                    "# macOS Docker Installation:",
                    "brew install --cask docker",
                    "# Or download from: https://docs.docker.com/desktop/mac/install/",
                    "",
                ]
            )
        elif system == "linux":
            commands.extend(
                [
                    "# Linux Docker Installation:",
                    "sudo apt update",
                    "sudo apt install docker.io docker-compose",
                    "sudo systemctl start docker",
                    "sudo systemctl enable docker",
                    "sudo usermod -aG docker $USER",
                    "# Log out and back in for group changes to take effect",
                    "",
                ]
            )
        elif system == "windows":
            commands.extend(
                [
                    "# Windows Docker Installation:",
                    "# Download Docker Desktop from: https://docs.docker.com/desktop/windows/install/",
                    "# Or use chocolatey: choco install docker-desktop",
                    "",
                ]
            )
        commands.extend(
            [
                "# Build and run the Kotlin MCP Server:",
                "./docker-setup.sh build",
                "./docker-setup.sh start",
                "",
                "# Or manually:",
                "docker build -t kotlin-mcp-server .",
                "docker-compose up -d kotlin-mcp-server",
            ]
        )
        return commands
    def run_validation(self) -> bool:
        """Run complete Docker validation"""
        print("🚀 Starting Docker validation for Kotlin MCP Server")
        print("=" * 60)
        success = True
        # Check Docker installation
        docker_installed, docker_msg = self.check_docker_installation()
        if not docker_installed:
            self.warnings.append(f"Docker: {docker_msg}")
        compose_installed, compose_msg = self.check_docker_compose_installation()
        if not compose_installed:
            self.warnings.append(f"Docker Compose: {compose_msg}")
        # Check configuration files
        success &= self.check_docker_files()
        success &= self.check_docker_compose()
        success &= self.check_dockerignore()
        success &= self.check_requirements_compatibility()
        success &= self.validate_build_context()
        # Print results
        print("\n" + "=" * 60)
        print("📊 VALIDATION RESULTS")
        print("=" * 60)
        if self.issues:
            print("❌ ISSUES FOUND:")
            for issue in self.issues:
                print(f"   • {issue}")
            print()
        if self.warnings:
            print("⚠️  WARNINGS:")
            for warning in self.warnings:
                print(f"   • {warning}")
            print()
        if self.recommendations:
            print("💡 RECOMMENDATIONS:")
            for rec in self.recommendations:
                print(f"   • {rec}")
            print()
        if not docker_installed or not compose_installed:
            print("🛠️  SETUP COMMANDS:")
            for cmd in self.generate_setup_commands():
                print(f"   {cmd}")
            print()
        if success and not self.issues:
            print("🎉 Docker configuration is valid!")
            if docker_installed and compose_installed:
                print("✅ Ready to build and run with Docker!")
            else:
                print("📦 Install Docker to proceed with containerized deployment")
        else:
            print("🔧 Please fix the issues above before proceeding")
        return success and not self.issues
def main() -> int:
    """Main validation function"""
    validator = DockerValidator()
    success = validator.run_validation()
    return 0 if success else 1
if __name__ == "__main__":
    sys.exit(main())