CoolFace
Apppublic

Hamza4100/ai-workflow-agent

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
docker_helper.py450 linesDownload Raw Back to tools
1# Docker Helper Tool
2"""
3Clone repositories and manage Docker builds.
4Includes error analysis and fix suggestions.
5"""
6
7import httpx
8import subprocess
9import os
10import shutil
11import logging
12from typing import Dict, Any, Optional, List
13from pathlib import Path
14
15from config import settings
16
17logger = logging.getLogger(__name__)
18
19
20class DockerHelper:
21    """
22    Docker automation helper.
23    Clones repos, builds containers, analyzes errors, suggests fixes.
24    """
25    
26    def __init__(self):
27        self.projects_dir = Path(settings.PROJECTS_DIR)
28        self.ollama_host = settings.OLLAMA_HOST
29        self.ollama_model = settings.OLLAMA_MODEL
30        self.client = httpx.AsyncClient(timeout=120.0)
31        
32        # Ensure projects directory exists
33        self.projects_dir.mkdir(parents=True, exist_ok=True)
34    
35    async def clone_and_build(
36        self,
37        repo_url: str,
38        branch: str = "main"
39    ) -> Dict[str, Any]:
40        """
41        Clone repository and attempt Docker build.
42        
43        Args:
44            repo_url: GitHub repository URL
45            branch: Branch to clone (default: main)
46            
47        Returns:
48            Dict with success status, logs, and fix suggestions
49        """
50        project_name = self._extract_project_name(repo_url)
51        project_path = self.projects_dir / project_name
52        
53        try:
54            # Step 1: Clone repository
55            clone_result = await self._clone_repo(repo_url, project_path, branch)
56            if not clone_result["success"]:
57                return clone_result
58            
59            # Step 2: Detect project structure
60            structure = await self._detect_structure(project_path)
61            
62            # Step 3: Attempt Docker build
63            build_result = await self._docker_build(project_path, structure)
64            
65            if build_result["success"]:
66                return {
67                    "success": True,
68                    "message": f"Project {project_name} built successfully",
69                    "container_id": build_result.get("container_id"),
70                    "logs": build_result.get("logs", "")
71                }
72            else:
73                # Step 4: Analyze error and suggest fix
74                fix = await self._analyze_and_suggest_fix(
75                    build_result.get("logs", ""),
76                    project_path
77                )
78                
79                return {
80                    "success": False,
81                    "message": f"Build failed for {project_name}",
82                    "logs": build_result.get("logs", ""),
83                    "fix_suggestion": fix
84                }
85                
86        except Exception as e:
87            logger.error(f"Clone and build error: {e}")
88            return {
89                "success": False,
90                "message": f"Error: {str(e)}",
91                "logs": str(e)
92            }
93    
94    def _extract_project_name(self, repo_url: str) -> str:
95        """Extract project name from repository URL."""
96        # Handle various URL formats
97        url = repo_url.rstrip("/")
98        if url.endswith(".git"):
99            url = url[:-4]
100        return url.split("/")[-1]
101    
102    async def _clone_repo(
103        self,
104        repo_url: str,
105        project_path: Path,
106        branch: str
107    ) -> Dict[str, Any]:
108        """Clone repository to local directory."""
109        try:
110            # Remove existing directory if present
111            if project_path.exists():
112                shutil.rmtree(project_path)
113            
114            # Clone repository
115            result = subprocess.run(
116                ["git", "clone", "--depth", "1", "-b", branch, repo_url, str(project_path)],
117                capture_output=True,
118                text=True,
119                timeout=120
120            )
121            
122            if result.returncode == 0:
123                logger.info(f"Cloned {repo_url} to {project_path}")
124                return {"success": True, "message": "Repository cloned"}
125            else:
126                # Try without branch specification (use default)
127                result = subprocess.run(
128                    ["git", "clone", "--depth", "1", repo_url, str(project_path)],
129                    capture_output=True,
130                    text=True,
131                    timeout=120
132                )
133                
134                if result.returncode == 0:
135                    return {"success": True, "message": "Repository cloned (default branch)"}
136                else:
137                    return {
138                        "success": False,
139                        "message": f"Clone failed: {result.stderr}",
140                        "logs": result.stderr
141                    }
142                    
143        except subprocess.TimeoutExpired:
144            return {"success": False, "message": "Clone timed out"}
145        except Exception as e:
146            return {"success": False, "message": f"Clone error: {str(e)}"}
147    
148    async def _detect_structure(self, project_path: Path) -> Dict[str, Any]:
149        """Detect project structure and configuration files."""
150        structure = {
151            "has_dockerfile": False,
152            "has_compose": False,
153            "has_requirements": False,
154            "has_package_json": False,
155            "has_makefile": False,
156            "dockerfile_path": None,
157            "compose_path": None,
158            "language": "unknown"
159        }
160        
161        files_to_check = {
162            "Dockerfile": ("has_dockerfile", "dockerfile_path"),
163            "docker-compose.yml": ("has_compose", "compose_path"),
164            "docker-compose.yaml": ("has_compose", "compose_path"),
165            "compose.yml": ("has_compose", "compose_path"),
166            "compose.yaml": ("has_compose", "compose_path"),
167            "requirements.txt": ("has_requirements", None),
168            "package.json": ("has_package_json", None),
169            "Makefile": ("has_makefile", None)
170        }
171        
172        for filename, (flag, path_key) in files_to_check.items():
173            file_path = project_path / filename
174            if file_path.exists():
175                structure[flag] = True
176                if path_key:
177                    structure[path_key] = str(file_path)
178        
179        # Detect language
180        if structure["has_requirements"]:
181            structure["language"] = "python"
182        elif structure["has_package_json"]:
183            structure["language"] = "javascript"
184        
185        return structure
186    
187    async def _docker_build(
188        self,
189        project_path: Path,
190        structure: Dict[str, Any]
191    ) -> Dict[str, Any]:
192        """Attempt to build Docker container."""
193        try:
194            project_name = project_path.name.lower().replace("_", "-").replace(".", "-")
195            
196            # Prefer docker-compose if available
197            if structure["has_compose"]:
198                compose_path = structure["compose_path"]
199                
200                result = subprocess.run(
201                    ["docker", "compose", "-f", compose_path, "build"],
202                    capture_output=True,
203                    text=True,
204                    cwd=str(project_path),
205                    timeout=600  # 10 minute timeout
206                )
207                
208                if result.returncode == 0:
209                    # Start containers
210                    start_result = subprocess.run(
211                        ["docker", "compose", "-f", compose_path, "up", "-d"],
212                        capture_output=True,
213                        text=True,
214                        cwd=str(project_path),
215                        timeout=300
216                    )
217                    
218                    return {
219                        "success": start_result.returncode == 0,
220                        "logs": result.stdout + start_result.stdout,
221                        "method": "docker-compose"
222                    }
223                else:
224                    return {
225                        "success": False,
226                        "logs": result.stderr,
227                        "method": "docker-compose"
228                    }
229            
230            # Fall back to Dockerfile
231            elif structure["has_dockerfile"]:
232                result = subprocess.run(
233                    ["docker", "build", "-t", project_name, "."],
234                    capture_output=True,
235                    text=True,
236                    cwd=str(project_path),
237                    timeout=600
238                )
239                
240                if result.returncode == 0:
241                    # Run container
242                    run_result = subprocess.run(
243                        ["docker", "run", "-d", "--name", f"{project_name}-container", project_name],
244                        capture_output=True,
245                        text=True,
246                        timeout=60
247                    )
248                    
249                    return {
250                        "success": run_result.returncode == 0,
251                        "container_id": run_result.stdout.strip()[:12] if run_result.returncode == 0 else None,
252                        "logs": result.stdout + run_result.stdout,
253                        "method": "dockerfile"
254                    }
255                else:
256                    return {
257                        "success": False,
258                        "logs": result.stderr,
259                        "method": "dockerfile"
260                    }
261            
262            # No Docker configuration found - generate Dockerfile
263            else:
264                generated = await self._generate_dockerfile(project_path, structure)
265                if generated:
266                    # Retry build with generated Dockerfile
267                    structure["has_dockerfile"] = True
268                    structure["dockerfile_path"] = str(project_path / "Dockerfile")
269                    return await self._docker_build(project_path, structure)
270                else:
271                    return {
272                        "success": False,
273                        "logs": "No Dockerfile found and auto-generation failed",
274                        "method": "none"
275                    }
276                    
277        except subprocess.TimeoutExpired:
278            return {"success": False, "logs": "Build timed out (>10 minutes)"}
279        except Exception as e:
280            return {"success": False, "logs": f"Build error: {str(e)}"}
281    
282    async def _generate_dockerfile(
283        self,
284        project_path: Path,
285        structure: Dict[str, Any]
286    ) -> bool:
287        """Generate a Dockerfile based on project structure."""
288        try:
289            dockerfile_content = ""
290            
291            if structure["language"] == "python":
292                dockerfile_content = """# Auto-generated Dockerfile
293FROM python:3.11-slim
294
295WORKDIR /app
296
297COPY requirements.txt .
298RUN pip install --no-cache-dir -r requirements.txt
299
300COPY . .
301
302CMD ["python", "main.py"]
303"""
304            elif structure["language"] == "javascript":
305                dockerfile_content = """# Auto-generated Dockerfile
306FROM node:20-alpine
307
308WORKDIR /app
309
310COPY package*.json ./
311RUN npm install
312
313COPY . .
314
315EXPOSE 3000
316
317CMD ["npm", "start"]
318"""
319            else:
320                # Generic fallback
321                dockerfile_content = """# Auto-generated Dockerfile
322FROM ubuntu:22.04
323
324WORKDIR /app
325
326COPY . .
327
328CMD ["bash"]
329"""
330            
331            dockerfile_path = project_path / "Dockerfile"
332            dockerfile_path.write_text(dockerfile_content)
333            
334            logger.info(f"Generated Dockerfile for {project_path.name}")
335            return True
336            
337        except Exception as e:
338            logger.error(f"Dockerfile generation error: {e}")
339            return False
340    
341    async def _analyze_and_suggest_fix(
342        self,
343        error_logs: str,
344        project_path: Path
345    ) -> str:
346        """
347        Analyze build error and suggest fix using LLM.
348        
349        NOTE: This only suggests ONE fix, no infinite loops.
350        """
351        try:
352            prompt = f"""Analyze this Docker build error and suggest ONE specific fix.
353
354Error logs:
355```
356{error_logs[:2000]}
357```
358
359Project: {project_path.name}
360
361Provide a concise fix suggestion. If multiple issues, focus on the first/most critical one.
362Format:
3631. Problem: [what went wrong]
3642. Fix: [specific action to take]
3653. Command: [if applicable, the command to run]"""
366
367            response = await self.client.post(
368                f"{self.ollama_host}/api/generate",
369                json={
370                    "model": self.ollama_model,
371                    "prompt": prompt,
372                    "stream": False
373                }
374            )
375            
376            if response.status_code == 200:
377                result = response.json()
378                return result.get("response", "Unable to analyze error")
379            else:
380                return "Error analysis unavailable (LLM request failed)"
381                
382        except Exception as e:
383            logger.error(f"Error analysis failed: {e}")
384            return f"Error analysis failed: {str(e)}"
385    
386    async def get_container_logs(self, container_id: str, lines: int = 100) -> str:
387        """Get logs from a running container."""
388        try:
389            result = subprocess.run(
390                ["docker", "logs", "--tail", str(lines), container_id],
391                capture_output=True,
392                text=True,
393                timeout=30
394            )
395            return result.stdout + result.stderr
396        except Exception as e:
397            return f"Failed to get logs: {str(e)}"
398    
399    async def list_containers(self, all_containers: bool = False) -> List[Dict[str, str]]:
400        """List Docker containers."""
401        try:
402            cmd = ["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}"]
403            if all_containers:
404                cmd.append("-a")
405            
406            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
407            
408            containers = []
409            for line in result.stdout.strip().split("\n"):
410                if line:
411                    parts = line.split("\t")
412                    if len(parts) >= 4:
413                        containers.append({
414                            "id": parts[0],
415                            "name": parts[1],
416                            "status": parts[2],
417                            "image": parts[3]
418                        })
419            return containers
420            
421        except Exception as e:
422            logger.error(f"List containers error: {e}")
423            return []
424    
425    async def stop_container(self, container_id: str) -> bool:
426        """Stop a running container."""
427        try:
428            result = subprocess.run(
429                ["docker", "stop", container_id],
430                capture_output=True,
431                text=True,
432                timeout=60
433            )
434            return result.returncode == 0
435        except Exception:
436            return False
437    
438    async def remove_container(self, container_id: str) -> bool:
439        """Remove a container."""
440        try:
441            result = subprocess.run(
442                ["docker", "rm", "-f", container_id],
443                capture_output=True,
444                text=True,
445                timeout=60
446            )
447            return result.returncode == 0
448        except Exception:
449            return False
450