f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
"""Webhook-сервер для автодеплоя из Gitea (push → redeploy)."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Optional
|
|
|
|
from sqlmodel import select
|
|
|
|
from app.core import servers as servers_core
|
|
from app.db.database import get_session
|
|
from app.db.models import WebhookBinding
|
|
|
|
_server = None
|
|
|
|
|
|
def list_bindings() -> list[WebhookBinding]:
|
|
with get_session() as s:
|
|
return list(s.exec(select(WebhookBinding)).all())
|
|
|
|
|
|
def add_binding(**fields) -> WebhookBinding:
|
|
with get_session() as s:
|
|
b = WebhookBinding(**fields)
|
|
s.add(b)
|
|
s.commit()
|
|
s.refresh(b)
|
|
return b
|
|
|
|
|
|
def delete_binding(bid: int) -> None:
|
|
with get_session() as s:
|
|
b = s.get(WebhookBinding, bid)
|
|
if b:
|
|
s.delete(b)
|
|
s.commit()
|
|
|
|
|
|
async def _handler(reader, writer):
|
|
"""Минимальный HTTP-сервер для /webhooks/gitea."""
|
|
try:
|
|
req = await reader.readuntil(b"\r\n\r\n")
|
|
head = req.decode(errors="replace")
|
|
first = head.splitlines()[0] if head else ""
|
|
# тело
|
|
length = 0
|
|
for line in head.splitlines():
|
|
if line.lower().startswith("content-length:"):
|
|
length = int(line.split(":", 1)[1].strip())
|
|
body = await reader.readexactly(length) if length else b""
|
|
|
|
status, text = 200, "ok"
|
|
if first.startswith("POST /webhooks/gitea"):
|
|
try:
|
|
payload = json.loads(body or b"{}")
|
|
repo = payload.get("repository", {}).get("full_name", "")
|
|
ref = payload.get("ref", "") # refs/heads/main
|
|
branch = ref.rsplit("/", 1)[-1]
|
|
asyncio.ensure_future(_deploy(repo, branch))
|
|
text = f"deploy scheduled: {repo}@{branch}"
|
|
except Exception as e:
|
|
status, text = 400, f"bad payload: {e}"
|
|
else:
|
|
status, text = 404, "not found"
|
|
|
|
writer.write(f"HTTP/1.1 {status} OK\r\nContent-Length: {len(text)}\r\n"
|
|
f"Connection: close\r\n\r\n{text}".encode())
|
|
await writer.drain()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
try:
|
|
writer.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
async def _deploy(repo: str, branch: str) -> None:
|
|
with get_session() as s:
|
|
bindings = list(s.exec(select(WebhookBinding)
|
|
.where(WebhookBinding.repo == repo,
|
|
WebhookBinding.branch == branch,
|
|
WebhookBinding.enabled == True)).all()) # noqa: E712
|
|
for b in bindings:
|
|
srv = servers_core.get_server(b.server_id)
|
|
if not srv:
|
|
continue
|
|
try:
|
|
from app.core import gitea as g
|
|
from app.core.ssh import SSHClient
|
|
from app.core.tasks import tasks
|
|
async def job(log, srv=srv, b=b):
|
|
async with SSHClient(srv) as c:
|
|
cfg = g.get_config()
|
|
clone_url = f"{cfg['url']}/{repo}.git"
|
|
await g.deploy(c, clone_url, repo.split("/")[-1],
|
|
b.branch, b.compose_path, b.env or None, log)
|
|
tasks.start("webhook", f"auto-deploy {repo}@{branch}", srv.id, job)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
async def start(port: int = 9100) -> None:
|
|
global _server
|
|
if _server:
|
|
return
|
|
_server = await asyncio.start_server(_handler, "0.0.0.0", port)
|
|
|
|
|
|
def is_running() -> bool:
|
|
return _server is not None
|