ai-workflow-agent / tools /docker_helper.py
Hamza4100's picture
Upload 22 files
9c95c55 verified
# Docker Helper Tool
"""
Clone repositories and manage Docker builds.
Includes error analysis and fix suggestions.
"""
import httpx
import subprocess
import os
import shutil
import logging
from typing import Dict, Any, Optional, List
from pathlib import Path
from config import settings
logger = logging.getLogger(__name__)
class DockerHelper:
"""
Docker automation helper.
Clones repos, builds containers, analyzes errors, suggests fixes.
"""
def __init__(self):
self.projects_dir = Path(settings.PROJECTS_DIR)
self.ollama_host = settings.OLLAMA_HOST
self.ollama_model = settings.OLLAMA_MODEL
self.client = httpx.AsyncClient(timeout=120.0)
# Ensure projects directory exists
self.projects_dir.mkdir(parents=True, exist_ok=True)
async def clone_and_build(
self,
repo_url: str,
branch: str = "main"
) -> Dict[str, Any]:
"""
Clone repository and attempt Docker build.
Args:
repo_url: GitHub repository URL
branch: Branch to clone (default: main)
Returns:
Dict with success status, logs, and fix suggestions
"""
project_name = self._extract_project_name(repo_url)
project_path = self.projects_dir / project_name
try:
# Step 1: Clone repository
clone_result = await self._clone_repo(repo_url, project_path, branch)
if not clone_result["success"]:
return clone_result
# Step 2: Detect project structure
structure = await self._detect_structure(project_path)
# Step 3: Attempt Docker build
build_result = await self._docker_build(project_path, structure)
if build_result["success"]:
return {
"success": True,
"message": f"Project {project_name} built successfully",
"container_id": build_result.get("container_id"),
"logs": build_result.get("logs", "")
}
else:
# Step 4: Analyze error and suggest fix
fix = await self._analyze_and_suggest_fix(
build_result.get("logs", ""),
project_path
)
return {
"success": False,
"message": f"Build failed for {project_name}",
"logs": build_result.get("logs", ""),
"fix_suggestion": fix
}
except Exception as e:
logger.error(f"Clone and build error: {e}")
return {
"success": False,
"message": f"Error: {str(e)}",
"logs": str(e)
}
def _extract_project_name(self, repo_url: str) -> str:
"""Extract project name from repository URL."""
# Handle various URL formats
url = repo_url.rstrip("/")
if url.endswith(".git"):
url = url[:-4]
return url.split("/")[-1]
async def _clone_repo(
self,
repo_url: str,
project_path: Path,
branch: str
) -> Dict[str, Any]:
"""Clone repository to local directory."""
try:
# Remove existing directory if present
if project_path.exists():
shutil.rmtree(project_path)
# Clone repository
result = subprocess.run(
["git", "clone", "--depth", "1", "-b", branch, repo_url, str(project_path)],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
logger.info(f"Cloned {repo_url} to {project_path}")
return {"success": True, "message": "Repository cloned"}
else:
# Try without branch specification (use default)
result = subprocess.run(
["git", "clone", "--depth", "1", repo_url, str(project_path)],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
return {"success": True, "message": "Repository cloned (default branch)"}
else:
return {
"success": False,
"message": f"Clone failed: {result.stderr}",
"logs": result.stderr
}
except subprocess.TimeoutExpired:
return {"success": False, "message": "Clone timed out"}
except Exception as e:
return {"success": False, "message": f"Clone error: {str(e)}"}
async def _detect_structure(self, project_path: Path) -> Dict[str, Any]:
"""Detect project structure and configuration files."""
structure = {
"has_dockerfile": False,
"has_compose": False,
"has_requirements": False,
"has_package_json": False,
"has_makefile": False,
"dockerfile_path": None,
"compose_path": None,
"language": "unknown"
}
files_to_check = {
"Dockerfile": ("has_dockerfile", "dockerfile_path"),
"docker-compose.yml": ("has_compose", "compose_path"),
"docker-compose.yaml": ("has_compose", "compose_path"),
"compose.yml": ("has_compose", "compose_path"),
"compose.yaml": ("has_compose", "compose_path"),
"requirements.txt": ("has_requirements", None),
"package.json": ("has_package_json", None),
"Makefile": ("has_makefile", None)
}
for filename, (flag, path_key) in files_to_check.items():
file_path = project_path / filename
if file_path.exists():
structure[flag] = True
if path_key:
structure[path_key] = str(file_path)
# Detect language
if structure["has_requirements"]:
structure["language"] = "python"
elif structure["has_package_json"]:
structure["language"] = "javascript"
return structure
async def _docker_build(
self,
project_path: Path,
structure: Dict[str, Any]
) -> Dict[str, Any]:
"""Attempt to build Docker container."""
try:
project_name = project_path.name.lower().replace("_", "-").replace(".", "-")
# Prefer docker-compose if available
if structure["has_compose"]:
compose_path = structure["compose_path"]
result = subprocess.run(
["docker", "compose", "-f", compose_path, "build"],
capture_output=True,
text=True,
cwd=str(project_path),
timeout=600 # 10 minute timeout
)
if result.returncode == 0:
# Start containers
start_result = subprocess.run(
["docker", "compose", "-f", compose_path, "up", "-d"],
capture_output=True,
text=True,
cwd=str(project_path),
timeout=300
)
return {
"success": start_result.returncode == 0,
"logs": result.stdout + start_result.stdout,
"method": "docker-compose"
}
else:
return {
"success": False,
"logs": result.stderr,
"method": "docker-compose"
}
# Fall back to Dockerfile
elif structure["has_dockerfile"]:
result = subprocess.run(
["docker", "build", "-t", project_name, "."],
capture_output=True,
text=True,
cwd=str(project_path),
timeout=600
)
if result.returncode == 0:
# Run container
run_result = subprocess.run(
["docker", "run", "-d", "--name", f"{project_name}-container", project_name],
capture_output=True,
text=True,
timeout=60
)
return {
"success": run_result.returncode == 0,
"container_id": run_result.stdout.strip()[:12] if run_result.returncode == 0 else None,
"logs": result.stdout + run_result.stdout,
"method": "dockerfile"
}
else:
return {
"success": False,
"logs": result.stderr,
"method": "dockerfile"
}
# No Docker configuration found - generate Dockerfile
else:
generated = await self._generate_dockerfile(project_path, structure)
if generated:
# Retry build with generated Dockerfile
structure["has_dockerfile"] = True
structure["dockerfile_path"] = str(project_path / "Dockerfile")
return await self._docker_build(project_path, structure)
else:
return {
"success": False,
"logs": "No Dockerfile found and auto-generation failed",
"method": "none"
}
except subprocess.TimeoutExpired:
return {"success": False, "logs": "Build timed out (>10 minutes)"}
except Exception as e:
return {"success": False, "logs": f"Build error: {str(e)}"}
async def _generate_dockerfile(
self,
project_path: Path,
structure: Dict[str, Any]
) -> bool:
"""Generate a Dockerfile based on project structure."""
try:
dockerfile_content = ""
if structure["language"] == "python":
dockerfile_content = """# Auto-generated Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
"""
elif structure["language"] == "javascript":
dockerfile_content = """# Auto-generated Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
"""
else:
# Generic fallback
dockerfile_content = """# Auto-generated Dockerfile
FROM ubuntu:22.04
WORKDIR /app
COPY . .
CMD ["bash"]
"""
dockerfile_path = project_path / "Dockerfile"
dockerfile_path.write_text(dockerfile_content)
logger.info(f"Generated Dockerfile for {project_path.name}")
return True
except Exception as e:
logger.error(f"Dockerfile generation error: {e}")
return False
async def _analyze_and_suggest_fix(
self,
error_logs: str,
project_path: Path
) -> str:
"""
Analyze build error and suggest fix using LLM.
NOTE: This only suggests ONE fix, no infinite loops.
"""
try:
prompt = f"""Analyze this Docker build error and suggest ONE specific fix.
Error logs:
```
{error_logs[:2000]}
```
Project: {project_path.name}
Provide a concise fix suggestion. If multiple issues, focus on the first/most critical one.
Format:
1. Problem: [what went wrong]
2. Fix: [specific action to take]
3. Command: [if applicable, the command to run]"""
response = await self.client.post(
f"{self.ollama_host}/api/generate",
json={
"model": self.ollama_model,
"prompt": prompt,
"stream": False
}
)
if response.status_code == 200:
result = response.json()
return result.get("response", "Unable to analyze error")
else:
return "Error analysis unavailable (LLM request failed)"
except Exception as e:
logger.error(f"Error analysis failed: {e}")
return f"Error analysis failed: {str(e)}"
async def get_container_logs(self, container_id: str, lines: int = 100) -> str:
"""Get logs from a running container."""
try:
result = subprocess.run(
["docker", "logs", "--tail", str(lines), container_id],
capture_output=True,
text=True,
timeout=30
)
return result.stdout + result.stderr
except Exception as e:
return f"Failed to get logs: {str(e)}"
async def list_containers(self, all_containers: bool = False) -> List[Dict[str, str]]:
"""List Docker containers."""
try:
cmd = ["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}"]
if all_containers:
cmd.append("-a")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
containers = []
for line in result.stdout.strip().split("\n"):
if line:
parts = line.split("\t")
if len(parts) >= 4:
containers.append({
"id": parts[0],
"name": parts[1],
"status": parts[2],
"image": parts[3]
})
return containers
except Exception as e:
logger.error(f"List containers error: {e}")
return []
async def stop_container(self, container_id: str) -> bool:
"""Stop a running container."""
try:
result = subprocess.run(
["docker", "stop", container_id],
capture_output=True,
text=True,
timeout=60
)
return result.returncode == 0
except Exception:
return False
async def remove_container(self, container_id: str) -> bool:
"""Remove a container."""
try:
result = subprocess.run(
["docker", "rm", "-f", container_id],
capture_output=True,
text=True,
timeout=60
)
return result.returncode == 0
except Exception:
return False