feat: Phase 6, Device provisioning and deployment of updates on git-pull
This commit is contained in:
0
backend/admin/__init__.py
Normal file
0
backend/admin/__init__.py
Normal file
64
backend/admin/router.py
Normal file
64
backend/admin/router.py
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
cmd = f"cd {project_path} && git pull origin main && docker compose up -d --build"
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=300)
|
||||
output = stdout.decode(errors="replace") if stdout else ""
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.error(f"Deploy failed (exit {proc.returncode}):\n{output}")
|
||||
raise HTTPException(status_code=500, detail=f"Deploy script failed:\n{output[-500:]}")
|
||||
|
||||
logger.info(f"Deploy succeeded:\n{output[-300:]}")
|
||||
return {"ok": True, "output": output[-1000:]}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPException(status_code=504, detail="Deploy timed out after 300 seconds")
|
||||
Reference in New Issue
Block a user