Files
bellsystems-cp/backend/admin/router.py

78 lines
2.7 KiB
Python

import asyncio
import hashlib
import hmac
import logging
from fastapi import APIRouter, HTTPException, Request
from config import settings
logger = logging.getLogger("admin.deploy")
router = APIRouter(prefix="/api/admin", tags=["admin"])
@router.post("/deploy")
async def deploy(request: Request):
"""Gitea webhook endpoint — pulls latest code and rebuilds Docker containers.
Gitea webhook configuration:
URL: https://<your-domain>/api/admin/deploy
Secret token: value of DEPLOY_SECRET env var
Content-Type: application/json
Trigger: Push events only (branch: main)
Add to VPS .env:
DEPLOY_SECRET=<random-strong-token>
DEPLOY_PROJECT_PATH=/home/bellsystems/bellsystems-cp
"""
if not settings.deploy_secret:
raise HTTPException(status_code=503, detail="Deploy secret not configured on server")
# Gitea sends the HMAC-SHA256 of the request body in X-Gitea-Signature
sig_header = request.headers.get("X-Gitea-Signature", "")
body = await request.body()
expected_sig = hmac.new(
key=settings.deploy_secret.encode(),
msg=body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig_header, expected_sig):
raise HTTPException(status_code=403, detail="Invalid webhook signature")
logger.info("Auto-deploy triggered via Gitea webhook")
project_path = settings.deploy_project_path
# Write a deploy script to the host filesystem (via the mounted project path)
# then execute it with nsenter into the host's PID namespace so it runs as
# a host process — not a container child — and survives container restarts.
script_path = f"{project_path}/deploy.sh"
log_path = f"{project_path}/deploy.log"
script = (
f"#!/bin/sh\n"
f"exec > {log_path} 2>&1\n"
f"echo \"Deploy started at $(date)\"\n"
f"git config --global --add safe.directory {project_path}\n"
f"cd {project_path}\n"
f"git fetch origin main\n"
f"git reset --hard origin/main\n"
f"docker-compose up -d --build\n"
f"echo \"Deploy finished at $(date)\"\n"
)
with open(script_path, "w") as f:
f.write(script)
# nsenter into host PID namespace (PID 1 = host init) so the process
# is owned by the host and survives this container restarting.
trigger_cmd = f"chmod +x {script_path} && nsenter -t 1 -m -u -i -n -p -- sh -c 'nohup {script_path} &'"
await asyncio.create_subprocess_shell(
trigger_cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
logger.info("Auto-deploy triggered on host via nsenter")
return {"ok": True, "message": "Deploy started"}