Files
tgit/docs/ARCHITECTURE.md
T

200 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура tgit
> Требования — в [`SPEC.md`](SPEC.md), решения и их обоснования — в [`DECISIONS.md`](DECISIONS.md).
## 1. Общая схема
```
Telegram ◄──long polling──┐
│
┌────────────┴─────────────────────────── хост (VPS / ноутбук) ─────────────┐
│ tgit-bot (один процесс, asyncio) │
│ ├─ bot/ aiogram: команды, загрузка файлов, кнопки │
│ ├─ uploads/ валидация → GitPublisher (очередь, git + SSH) ───────────┼──► SourceCraft
│ ├─ ci/ CIWatcher: polling прогонов по sha, логи кубов ◄────────┼─── REST API
│ ├─ runtime/ Monitor: события и метрики контейнеров, логи │
│ ├─ alerts/ правила, антиспам, доставка │
│ └─ llm/ LogAssistant: вопрос → LogQuery → поиск → ответ ─────────┼──► LLM API
│ │ │ │
│ ┌────┴─────┐ ┌────────┴────────┐ │
│ │ postgres │ │ docker-proxy │ (только чтение: ps/logs/ │
│ └──────────┘ │ socket-proxy │ events/stats) │
│ └────────┬────────┘ │
│ │ /var/run/docker.sock │
│ tgit-runner (скрипт на хосте) ─┤ │
│ ▲ ssh, forced command ▼ │
│ │ tgit-app-<project> контейнеры (песочница) │
└─────┼─────────────────────────────────────────────────────────────────────┘
│
SourceCraft CI (tgit-uploads): check → deploy ──┘
```
**Два репозитория:**
| Репозиторий | Что внутри | CI/CD |
|---|---|---|
| `tgit` (Gitea → зеркало в SourceCraft) | код бота | push в любую ветку: lint + types + tests; push `main`: + деплой бота на хост |
| `tgit-uploads` | `projects/<name>/…` от пользователей | push `main`: проверки проекта; успех → `ssh tgit-runner deploy <project> <sha>` |
## 2. Компоненты
### 2.1 `app/bot` — интерфейс Telegram
aiogram 3, роутеры по фичам (`upload`, `projects`, `logs`, `ask`, `admin`). Middleware: проверка доступа по белому
списку, логирование апдейтов без содержимого файлов. Длинные ответы → файл.
### 2.2 `app/uploads` — загрузка и коммит
- `validate(file) -> ValidatedUpload` — размер, кодировка, `ast.parse`, режим `# tgit: mode=…`.
- `GitPublisher` — держит локальный клон `tgit-uploads` в volume; **единственная очередь** (`asyncio.Queue`, один воркер),
чтобы пуши не конфликтовали. Шаги: `fetch` → `reset --hard origin/main` → запись файла → `commit` → `push`;
при отказе push — повтор с `pull --rebase` (до 3 раз). git вызывается через `asyncio.create_subprocess_exec`
с `GIT_SSH_COMMAND="ssh -i <deploy_key> -o IdentitiesOnly=yes"`.
- Пустой diff → «изменений нет», коммита нет.
### 2.3 `app/sourcecraft` — клиент API
`httpx.AsyncClient`, Bearer-токен, ретраи на сетевые ошибки и 5xx (экспоненциально, до 5 попыток),
типизированные ответы (pydantic). Методы: `list_runs`, `get_run`, `get_cube_logs`, `trigger_workflow`.
Подробности API — [`dev/SOURCECRAFT_API.md`](dev/SOURCECRAFT_API.md).
### 2.4 `app/ci` — наблюдатель за CI
Фоновая задача. Опрашивает `/cicd/runs` **только пока есть незавершённые загрузки** (интервал 10 с, бэкофф до 60 с
при ошибках). Сопоставляет прогон с загрузкой по sha коммита, на смене статуса редактирует сообщение пользователя.
При падении скачивает лог упавшего куба. Таймаут прогона — 30 мин → статус `timeout`.
Webhooks у SourceCraft нет — отсюда polling (ADR-003).
### 2.5 `app/runtime` — мониторинг приложений
Через `docker-socket-proxy` (только `GET` на containers/logs/events/stats):
- поток `events` с фильтром `label=tgit.project` → `die` (код выхода), `oom`, `restart`;
- раз в 15 с `stats` по контейнерам приложений → CPU% / RAM% от лимита, скользящее окно 1 мин;
- `logs` с `since`/`tail` — для `/logs` и LLM.
Управление (`stop`/`restart`) бот **не** делает через сокет — он вызывает `tgit-runner` (ADR-006).
### 2.6 `app/alerts` — алерты
Правила (пороги — в `config/alerts.yaml`) → событие → антиспам (ключ `project+kind`, окно 10 мин, счётчик) →
доставка владельцу и подписчикам. Системные алерты (недоступен API/Docker/БД) — администраторам.
### 2.7 `app/llm` — ассистент по логам
```
вопрос ──► LLM #1 (JSON-схема LogQuery) ──► валидация ──► поиск по логам ──► маскирование ──► LLM #2 ──► ответ
```
- `LogQuery`: `source` (`app` | `ci`), `since` (≤ 7 д), `pattern` (regex ≤ 200 символов), `level`, `limit` (≤ 500).
- Поиск — Python `regex` с таймаутом (защита от ReDoS), **никакого shell** (ADR-007).
- Маскирование: токены, `KEY=…`, `password=…`, JWT, e-mail, IP → `***`.
- Провайдер — за протоколом `LLMProvider` + фабрика по конфигу: OpenAI-совместимый API (YandexGPT, OpenAI, локальная
модель) и `mock` для тестов (ADR-008).
### 2.8 `app/db` — хранилище
SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLite.
| Таблица | Поля (основные) |
|---|---|
| `users` | `tg_id` PK, `username`, `role` (`user`/`admin`), `is_allowed`, `llm_quota_used`, `created_at` |
| `projects` | `id`, `name` UNIQUE, `owner_id` → users, `mode` (`service`/`job`), `network_enabled`, `alerts_enabled` |
| `uploads` | `id`, `project_id`, `user_id`, `filename`, `sha`, `status` (`committed`/`ci_running`/`ci_ok`/`ci_failed`/`deployed`/`timeout`), `chat_id`, `message_id`, `created_at` |
| `ci_runs` | `id`, `upload_id`, `run_slug`, `status`, `failed_cube`, `url`, `updated_at` |
| `alerts` | `id`, `project_id`, `kind`, `payload` JSON, `count`, `first_at`, `last_at`, `delivered_at` |
| `llm_requests` | `id`, `user_id`, `project_id`, `question`, `log_query` JSON, `tokens`, `created_at` |
### 2.9 Конфигурация
`app/config.py` — `pydantic-settings`, один класс на подсистему (`TelegramSettings`, `SourceCraftSettings`,
`DockerSettings`, `LLMSettings`, …), всё из `.env`. Пороги и тексты — `config/*.yaml`.
## 3. Потоки данных
### 3.1 Загрузка → CI → CD
```
user ──.py──► bot ── validate ──✗──► «ошибка, строка N»
│✓
▼
GitPublisher ──push──► tgit-uploads@main
│ │ (SourceCraft CI)
«✅ a1b2c3d, ⏳ CI» ◄┘ ├─ check: py_compile · ruff · pytest · bandit
└─ deploy: ssh tgit-deploy@host → tgit-runner deploy <project> <sha>
│
CIWatcher ──poll runs(sha)──► статус ──► edit message ▼
docker build + run tgit-app-<project>
Monitor ◄── events/stats ── docker-proxy ◄──────────────────┘
└──► alerts ──► user
```
### 3.2 Вопрос к логам
`/ask` → квота → LLM #1 → `LogQuery` → валидация → `runtime.logs()` / `sourcecraft.get_cube_logs()` → фильтр regex →
маскирование → LLM #2 (вопрос + найденные строки) → ответ + запрос + доказательства → запись в `llm_requests`.
## 4. Песочница приложений (`tgit-runner`)
`infra/runner/tgit-runner` — bash-скрипт на хосте; единственная команда, доступная ключу CI (`command="…"` в
`authorized_keys` пользователя `tgit-deploy`). Подкоманды: `deploy <project> <sha>`, `stop <project>`, `restart <project>`.
`deploy`: забирает архив проекта по sha из `tgit-uploads` → собирает образ `tgit-app-<project>:<sha>` из шаблона
(`python:3.12-slim` + `requirements.txt`) → запускает:
```
docker run -d --name tgit-app-<project> --label tgit.project=<project> --label tgit.sha=<sha>
--user 10001:10001 --read-only --tmpfs /tmp:size=16m
--cap-drop ALL --security-opt no-new-privileges
--memory 128m --memory-swap 128m --cpus 0.25 --pids-limit 64
--network none # или tgit-apps-egress, если сеть разрешена
--restart on-failure:3 # service; для job — без рестарта
--log-opt max-size=5m --log-opt max-file=2
```
## 5. CI/CD
### 5.1 Репозиторий бота (`tgit/.sourcecraft/ci.yaml`)
Код живёт в Gitea, SourceCraft получает его push-зеркалом и запускает CI/CD (ADR-010).
| Триггер | Workflow | Шаги |
|---|---|---|
| push в любую ветку (через зеркало Gitea; PR живут в Gitea — ADR-010) | `check` | `uv sync --frozen` → `ruff check` + `ruff format --check` → `mypy app` → `pytest --cov=app --cov-fail-under=70` |
| push в `main` | `check` + `deploy` | → `infra/deploy/deploy.sh` по SSH (секреты `DEPLOY_SSH_KEY`, `DEPLOY_HOST`) → smoke: healthcheck контейнера |
### 5.2 Репозиторий загрузок (`tgit-uploads/.sourcecraft/ci.yaml`)
Шаблон хранится в `tgit/infra/uploads-template/` и ставится в `tgit-uploads` один раз.
Определение изменённого проекта — `git diff --name-only HEAD~1 -- projects/` внутри куба.
## 6. Деплой
| Где | Как |
|---|---|
| Любая машина | `git clone` → `cp .env.example .env` → заполнить → `make up` |
| VPS vobot (основной) | push в `main` → CI → `infra/deploy/deploy.sh` → `/srv/tgit` (пользователь `tgit-deploy`, Compose-проект `tgit`) |
`deploy.sh` (по образцу meditron): `git archive` → `scp` → распаковка в `app.new` → перенос `.env` → атомарная замена
`app` → `docker compose -p tgit up -d --build` → ожидание healthcheck → запись `REVISION`. Все SSH-шаги с ретраями
(хост режет часть соединений). Хост задаётся `DEPLOY_HOST` — тот же скрипт работает для любого сервера.
Бюджет ресурсов на VPS (свободно ~1,9 ГБ RAM): бот ≤ 256 МБ, postgres ≤ 256 МБ, proxy ≤ 32 МБ,
приложения ≤ 10 × 128 МБ.
## 7. Структура репозитория
```
app/ код бота: bot, uploads, sourcecraft, ci, runtime, alerts, llm, db, config.py, main.py
config/ alerts.yaml, texts.yaml, llm-промпты
infra/
deploy/ deploy.sh, docker-compose.server.yml
runner/ tgit-runner, шаблон Dockerfile приложения, authorized_keys.example
uploads-template/ .sourcecraft/ci.yaml и README для tgit-uploads
examples/ демо-скрипты для загрузки (ok, syntax error, lint fail, crash, cpu hog)
tests/ pytest: unit + интеграционные с фейками
docs/ SPEC, ARCHITECTURE, DECISIONS, dev/, презентация
Dockerfile · docker-compose.yml · Makefile · pyproject.toml · .env.example
```
## 8. Что важно учесть
| Риск | Что делаем |
|---|---|
| Чужой код на одном сервере с прод-Vobot | Песочница §4, лимиты, сеть выключена; общий лимит 10 приложений; в перспективе — отдельный хост (ADR-005) |
| Docker-сокет = root на хосте | Бот — только через read-only proxy; запуск — через `tgit-runner` с forced command |
| Пользователь `tgit-deploy` в группе `docker` ≈ root | Ключ ограничен одной командой; `tgit-runner` валидирует аргументы (`^[a-z][a-z0-9-]{1,30}$`, sha hex) |
| Prompt injection через логи | LLM только возвращает `LogQuery`, который валидируется; ответ LLM — просто текст пользователю |
| ReDoS в regex от LLM | Модуль `regex` с таймаутом, лимит длины паттерна и объёма логов |
| Утечка секретов в LLM | Маскирование перед отправкой; секреты бота не попадают в логи |
| Нестабильный SourceCraft API | Ретраи, идемпотентность, проверка перед повторным POST |
| Гонки пушей | Одна очередь GitPublisher, `pull --rebase` при отказе |
| Лимиты Telegram (4096 символов, ~30 msg/s) | Длинное — файлом; редактирование одного сообщения вместо потока |
| Диск хоста занят на 75% | Ротация логов контейнеров, `docker image prune` старых ревизий приложений в `tgit-runner` |