Files
innotexBoard/backend/app/services/compose_manager.py
innotex c51592c7ea feat: Add Docker image update system (TrueNAS Scale inspired)
- Implement UpdateService for image version checking and atomic updates
- Add DockerComposeManager for centralized docker-compose management
- Create 12 docker-compose references in /home/innotex/Docker
- Add 13 new API endpoints (6 for images, 7 for compose management)
- Add comprehensive documentation and examples
2026-01-16 19:37:23 +01:00

277 lines
10 KiB
Python

import os
import subprocess
import json
from typing import List, Dict, Optional
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class DockerComposeManager:
"""Manager pour gérer les docker-compose localisés dans /home/innotex/Docker
Inspiré de TrueNAS Scale avec support pour:
- Découverte automatique des docker-compose
- Gestion centralisée des conteneurs
- Mise à jour des images
"""
DOCKER_COMPOSE_DIR = "/home/innotex/Docker"
def __init__(self):
self.docker_dir = Path(self.DOCKER_COMPOSE_DIR)
if not self.docker_dir.exists():
logger.warning(f"Répertoire Docker absent: {self.DOCKER_COMPOSE_DIR}")
def discover_compose_files(self) -> List[Dict[str, str]]:
"""Découvre tous les docker-compose.*.yml dans le répertoire"""
compose_files = []
if not self.docker_dir.exists():
return compose_files
try:
for file in sorted(self.docker_dir.glob("docker-compose.*.yml")):
app_name = file.stem.replace("docker-compose.", "")
compose_files.append({
"name": app_name,
"file": file.name,
"path": str(file),
"exists": file.exists()
})
except Exception as e:
logger.error(f"Erreur lors de la découverte des docker-compose: {e}")
return compose_files
def get_compose_status(self, compose_file: str) -> Dict:
"""Récupère l'état des conteneurs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
# Exécuter docker-compose ps
result = subprocess.run(
["docker-compose", "-f", str(file_path), "ps", "--format=json"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=10
)
if result.returncode != 0:
return {"status": "error", "message": result.stderr}
try:
containers = json.loads(result.stdout) if result.stdout else []
return {
"status": "success",
"file": compose_file,
"containers": containers,
"count": len(containers)
}
except json.JSONDecodeError:
# Fallback si le format JSON n'est pas disponible
return {
"status": "success",
"file": compose_file,
"output": result.stdout,
"count": len(result.stdout.strip().split('\n'))
}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors de la récupération du statut"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def start_compose(self, compose_file: str) -> Dict:
"""Démarre les conteneurs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "up", "-d"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=60
)
if result.returncode == 0:
return {
"status": "success",
"message": f"Services démarrés pour {compose_file}",
"output": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors du démarrage des services"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def stop_compose(self, compose_file: str) -> Dict:
"""Arrête les conteneurs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "stop"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=60
)
if result.returncode == 0:
return {
"status": "success",
"message": f"Services arrêtés pour {compose_file}",
"output": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors de l'arrêt des services"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def down_compose(self, compose_file: str) -> Dict:
"""Arrête et supprime les conteneurs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "down"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=60
)
if result.returncode == 0:
return {
"status": "success",
"message": f"Services arrêtés et supprimés pour {compose_file}",
"output": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors de la suppression des services"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def restart_compose(self, compose_file: str) -> Dict:
"""Redémarre les conteneurs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "restart"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=60
)
if result.returncode == 0:
return {
"status": "success",
"message": f"Services redémarrés pour {compose_file}",
"output": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors du redémarrage des services"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def pull_compose_images(self, compose_file: str) -> Dict:
"""Pull les images d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "pull"],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=300
)
if result.returncode == 0:
return {
"status": "success",
"message": f"Images téléchargées pour {compose_file}",
"output": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors du téléchargement des images"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}
def logs_compose(self, compose_file: str, tail: int = 100) -> Dict:
"""Récupère les logs d'un docker-compose"""
try:
file_path = self.docker_dir / compose_file
if not file_path.exists():
return {"status": "error", "message": f"Fichier non trouvé: {compose_file}"}
result = subprocess.run(
["docker-compose", "-f", str(file_path), "logs", "--tail", str(tail)],
capture_output=True,
text=True,
cwd=str(self.docker_dir),
timeout=10
)
if result.returncode == 0:
return {
"status": "success",
"file": compose_file,
"logs": result.stdout
}
else:
return {"status": "error", "message": result.stderr}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Timeout lors de la récupération des logs"}
except Exception as e:
logger.error(f"Erreur: {e}")
return {"status": "error", "message": str(e)}