f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""Gitea: репозитории, raw-файлы, деплой compose на сервер."""
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
|
|
import httpx
|
|
|
|
from app.core.ssh import SSHClient
|
|
from app.db.database import get_session
|
|
from app.db.models import Setting
|
|
|
|
DEFAULT_URL = "http://192.168.1.171:3000"
|
|
|
|
|
|
def _get(key: str) -> str:
|
|
with get_session() as s:
|
|
row = s.get(Setting, key)
|
|
return row.value if row else ""
|
|
|
|
|
|
def _set(key: str, value: str) -> None:
|
|
with get_session() as s:
|
|
row = s.get(Setting, key) or Setting(key=key)
|
|
row.value = value
|
|
s.add(row)
|
|
s.commit()
|
|
|
|
|
|
def get_config() -> dict:
|
|
return {"url": _get("gitea_url") or DEFAULT_URL, "token": _get("gitea_token")}
|
|
|
|
|
|
def save_config(url: str, token: str) -> None:
|
|
_set("gitea_url", url.rstrip("/"))
|
|
_set("gitea_token", token)
|
|
|
|
|
|
async def _client() -> httpx.AsyncClient:
|
|
cfg = get_config()
|
|
headers = {"Authorization": f"token {cfg['token']}"} if cfg["token"] else {}
|
|
return httpx.AsyncClient(base_url=cfg["url"], headers=headers, timeout=20)
|
|
|
|
|
|
async def list_repos() -> List[dict]:
|
|
async with await _client() as cl:
|
|
r = await cl.get("/api/v1/user/repos", params={"limit": 50})
|
|
r.raise_for_status()
|
|
return [{"name": x["name"], "full_name": x["full_name"],
|
|
"clone_url": x["clone_url"], "default_branch": x["default_branch"]}
|
|
for x in r.json()]
|
|
|
|
|
|
async def get_file(repo: str, path: str, ref: str = "main") -> str:
|
|
async with await _client() as cl:
|
|
r = await cl.get(f"/api/v1/repos/{repo}/raw/{path}", params={"ref": ref})
|
|
r.raise_for_status()
|
|
return r.text
|
|
|
|
|
|
async def test_connection() -> tuple[bool, str]:
|
|
try:
|
|
async with await _client() as cl:
|
|
r = await cl.get("/api/v1/version")
|
|
if r.status_code == 200:
|
|
return True, f"Gitea {r.json().get('version')}"
|
|
return False, f"HTTP {r.status_code}"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
|
|
# ---------- деплой compose ----------
|
|
|
|
async def deploy(c: SSHClient, clone_url: str, repo_name: str, branch: str,
|
|
compose_path: str, env_content: Optional[str], log) -> None:
|
|
"""git clone/pull в /opt/stacks/<repo> + docker compose up -d."""
|
|
base = "/opt/stacks"
|
|
workdir = f"{base}/{repo_name}"
|
|
await c.sudo(f"mkdir -p {base}")
|
|
|
|
r = await c.sudo(f"test -d {workdir}/.git && echo exists || echo missing")
|
|
if "exists" in r.stdout:
|
|
log(f"git pull в {workdir}")
|
|
r = await c.sudo(f"git -C {workdir} fetch --all && git -C {workdir} checkout {branch} "
|
|
f"&& git -C {workdir} pull origin {branch}", timeout=300)
|
|
else:
|
|
log(f"git clone {clone_url} → {workdir}")
|
|
url = clone_url
|
|
r = await c.sudo(f"git clone -b {branch} {url} {workdir}", timeout=300)
|
|
log(r.stdout[-1000:] + r.stderr[-500:])
|
|
if not r.ok:
|
|
raise RuntimeError("git операция не удалась")
|
|
|
|
if env_content is not None:
|
|
import base64
|
|
b64 = base64.b64encode(env_content.encode()).decode()
|
|
await c.sudo(f"echo {b64} | base64 -d > {workdir}/.env")
|
|
log(".env записан")
|
|
|
|
log("docker compose up -d")
|
|
r = await c.sudo(f"docker compose -f {workdir}/{compose_path} up -d", timeout=900)
|
|
log(r.stdout + r.stderr)
|
|
if not r.ok:
|
|
raise RuntimeError("compose up не удался")
|
|
|
|
|
|
async def stack_action(c: SSHClient, repo_name: str, compose_path: str, action: str) -> str:
|
|
assert action in ("up -d", "down", "restart", "pull", "logs --tail 100", "ps")
|
|
r = await c.sudo(f"docker compose -f /opt/stacks/{repo_name}/{compose_path} {action}",
|
|
timeout=600)
|
|
return r.stdout + r.stderr
|