feat: add applications management to the agent and backend
All checks were successful
Build and Push Docker Images / docker (push) Successful in 25s

- Updated agent to version 1.4.0 with new endpoints for managing Docker Compose applications.
- Implemented API requests in the backend for listing, creating, editing, and deploying applications.
- Introduced a new AppsModal component in the frontend for user interaction with applications.
- Added YAML editor for editing Docker Compose files with validation.
- Enhanced VpsCard component to include options for managing applications.
- Updated client API functions to support new application management features.
This commit is contained in:
jeanotx32
2026-08-01 02:11:24 -04:00
parent e277e99155
commit 71c17e3dc1
8 changed files with 978 additions and 13 deletions

View File

@@ -128,6 +128,18 @@ class ComposeUpdateRequest(BaseModel):
project: str
class ComposeWriteRequest(BaseModel):
content: str
deploy: bool = False
class AppCreateRequest(BaseModel):
name: str
content: str
filename: str = "compose.yaml"
deploy: bool = False
class RegisterRequest(BaseModel):
username: str
password: str
@@ -601,6 +613,25 @@ async def agent_post(vps: dict, path: str, payload: dict | None = None):
return await r.json()
async def agent_request(vps: dict, method: str, path: str,
payload: dict | None = None, timeout: int = AGENT_TIMEOUT):
"""Appel générique vers un agent — utilisé pour les verbes autres que GET/POST
et pour les opérations longues (déploiement compose)."""
url = f"http://{vps['host']}:{vps['port']}{path}"
headers = {"X-API-Key": vps["api_key"]}
async with aiohttp.ClientSession() as session:
async with session.request(
method, url, headers=headers, json=payload,
timeout=aiohttp.ClientTimeout(total=timeout),
) as r:
body = await r.json(content_type=None)
if r.status >= 400:
# Remonte le message de l'agent (validation compose, conflit…)
detail = body.get("detail") if isinstance(body, dict) else str(body)
raise HTTPException(status_code=r.status, detail=detail or f"HTTP {r.status}")
return body
async def fetch_vps_status(vps: dict) -> dict:
"""Interroge un agent et retourne son état complet.
@@ -1472,6 +1503,97 @@ async def container_action(
raise HTTPException(status_code=502, detail=str(e))
# ─── Applications compose ────────────────────────────────────────────────────
#
# Éditer un fichier compose revient à choisir ce qui tourne sur la machine :
# ces routes sont réservées aux administrateurs, comme le terminal.
DEPLOY_TIMEOUT = 900
def _require_vps(vps_id: str) -> dict:
vps = next((v for v in load_vps() if v["id"] == vps_id), None)
if not vps:
raise HTTPException(status_code=404, detail="VPS introuvable")
return vps
@app.get("/api/vps/{vps_id}/apps")
async def list_apps(vps_id: str, _: Annotated[dict, Depends(require_admin)]):
"""Liste les applications compose présentes sur le VPS."""
vps = _require_vps(vps_id)
try:
return await agent_request(vps, "GET", "/apps", timeout=15)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=502, detail=f"Agent injoignable ({e}). "
f"Version 1.4.0 de l'agent requise.")
@app.get("/api/vps/{vps_id}/apps/{name}/compose")
async def read_app_compose(vps_id: str, name: str, _: Annotated[dict, Depends(require_admin)]):
"""Retourne le contenu du fichier compose d'une application."""
vps = _require_vps(vps_id)
try:
return await agent_request(vps, "GET", f"/apps/{name}/compose", timeout=15)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@app.put("/api/vps/{vps_id}/apps/{name}/compose")
async def write_app_compose(
vps_id: str, name: str, body: ComposeWriteRequest,
_: Annotated[dict, Depends(require_admin)],
):
"""Enregistre le fichier compose, avec déploiement optionnel."""
vps = _require_vps(vps_id)
try:
return await agent_request(
vps, "PUT", f"/apps/{name}/compose",
payload={"content": body.content, "deploy": body.deploy},
timeout=DEPLOY_TIMEOUT if body.deploy else 30,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@app.post("/api/vps/{vps_id}/apps", status_code=201)
async def create_app(
vps_id: str, body: AppCreateRequest,
_: Annotated[dict, Depends(require_admin)],
):
"""Crée une application : dossier et fichier compose sur le VPS."""
vps = _require_vps(vps_id)
try:
return await agent_request(
vps, "POST", "/apps",
payload={"name": body.name, "content": body.content,
"filename": body.filename, "deploy": body.deploy},
timeout=DEPLOY_TIMEOUT if body.deploy else 30,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@app.post("/api/vps/{vps_id}/apps/{name}/up")
async def deploy_app(vps_id: str, name: str, _: Annotated[dict, Depends(require_admin)]):
"""Déploie une application existante (pull + up -d)."""
vps = _require_vps(vps_id)
try:
return await agent_request(vps, "POST", f"/apps/{name}/up", timeout=DEPLOY_TIMEOUT)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@app.post("/api/vps/{vps_id}/containers/{container_id}/exec/ticket")
async def container_exec_ticket(
vps_id: str, container_id: str,