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
This commit is contained in:
276
backend/app/services/compose_manager.py
Normal file
276
backend/app/services/compose_manager.py
Normal file
@@ -0,0 +1,276 @@
|
||||
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)}
|
||||
Reference in New Issue
Block a user