📡 API демона: gRPC (ConnectRPC) + REST
📌 Описание
API-сервер демона: один и тот же axum-роутер — gRPC (tonic; совместим с ConnectRPC-клиентами платформы) и REST (JSON поверх HTTP) — обслуживается на двух листенерах: TCP-порт для платформы (bearer-токен) и локальный unix-сокет для CLI (аутентификация по peer credentials, DMN-042). Все транспорты вызывают один сервисный слой; каждый вызов несёт контекст пользователя, который ограничивает видимость приложений (пользователь видит свои, root — все). Контракты — protobuf-файлы в proto/, они же источник правды для клиентов платформы (buf-зависимость).
🎯 Сценарии использования
- CLI
ascуправляет приложениями через unix-сокет без sudo и без группы docker:asc install,asc ls,asc app startработают у обычного пользователя и затрагивают только его приложения. - Платформа AdminService.Cloud управляет нодой через gRPC/ConnectRPC (через туннель nodeservice).
- Скрипты и сторонние интеграции используют REST:
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8420/v1/apps, либо сокет:curl --unix-socket /run/asc/asc.sock http://localhost/v1/apps(без токена — личность определяет ядро). - Перед открытием WebSocket-консоли клиент запрашивает временный консольный токен (
IssueConsoleToken) — платформа делает это автоматически после проверки прав.
🏗️ Техническое решение
Сервер
- TCP-листенер на
127.0.0.1:8420(настройка[api] listenв config.toml). Наружу порт не открывается — удалённый доступ идёт через туннель платформы (DMN-005 → tunnel). - Unix-сокет
/run/asc/asc.sock(настройка[api] socket; каталог создаёт systemd-юнит черезRuntimeDirectory=asc). Best-effort: если сокет не удаётся открыть, демон продолжает работать с одним TCP API и пишет предупреждение в лог. - Один роутер: axum REST + tonic-маршруты gRPC (h2c), диспетчеризация по пути/Content-Type.
- Все блокирующие операции (docker/systemctl/git) уходят в
spawn_blocking— event loop не блокируется.
🔐 Аутентификация и контекст пользователя
Каждый аутентифицированный запрос получает UserContext (uid, имя пользователя, признак root), который проставляет middleware транспорта; сервисный слой применяет из него правило владения приложениями — то же, что в in-process-режиме CLI (DMN-002).
TCP (платформа): bearer-токен.
- Генерируется демоном при первом старте (32 байта из CSPRNG), хранится в
api.tokenрядом с config.toml (root-only, 0600). - Обязателен для обоих транспортов: REST — заголовок
Authorization: Bearer <token>, gRPC — metadataauthorization. Сравнение — constant-time. - Без токена: REST →
401 {"error": ...}, gRPC →UNAUTHENTICATED. - Токен-вызовы действуют с полной видимостью (права пользователей платформа проверяет сама); пер-пользовательские API-токены — задача после MVP.
Unix-сокет (CLI): SO_PEERCRED. (DMN-042)
- Без токена. Демон запрашивает у ядра uid подключившегося процесса (
SO_PEERCRED) и строит контекст из него — ничто внутри запроса не может повысить привилегии. - Файл сокета нарочно доступен всем (0666): доступ к сокету сам по себе ничего не даёт, авторизация — peer uid, на каждый запрос. Обычный пользователь видит и управляет только своими приложениями; root-пир — всеми.
- Атрибуция
sudo asc ...: CLI передаётSUDO_UID/SUDO_USERзаголовкамиX-Asc-Sudo-Uid/X-Asc-Sudo-User; демон учитывает их только если сам пир — root, зеркаля in-process-поведение: новые приложения приписываются вызвавшему пользователю, sudo сохраняет полную видимость. - Маршрутизация CLI: команды жизненного цикла (
ls/status/install/app start|stop|restart|logs|remove|info) идут через сокет, когда он существует. Нет файла сокета — CLI работает in-process, как раньше (DMN-041: root по системным путям, пользователь в своём дереве~/.asc). Сокет есть, но не отвечает — ошибка для обычного пользователя и in-process-fallback с предупреждением для root (восстановление не должно зависеть от здоровья демона). - Известные ограничения (DMN-043): docker-контейнеры по-прежнему выполняются с привилегиями демона (root) — злонамеренный манифест может запросить опасные маунты, поэтому политика контейнеров для не-root владельцев — следующая задача; установка приватных репозиториев через демон использует git-креды демона, а не вызывающего;
asc attachпока открывает Docker напрямую и требует root.
🎫 Временные консольные токены
AppService.IssueConsoleToken(app_id, session)/POST /v1/apps/{id}/console-token {"session": "logs"|"attach"}.- Токен одноразовый, TTL 30 секунд, привязан к приложению и типу сессии; хранится в памяти демона.
- WebSocket-консоль (DMN-007) принимает подключение только по такому токену.
🗺️ Маршруты REST ↔ методы gRPC
| REST | gRPC | Описание |
|---|---|---|
GET /v1/status | DaemonService.GetStatus | Версия, счётчики приложений |
GET /v1/apps | AppService.ListApps | Список приложений (в объёме контекста вызывающего) |
POST /v1/apps {"spec": ..., "source"?, "name"?, "branch"?, "tag"?, "license_ack"?} | AppService.InstallApp | Установка из реестра или напрямую из git-URL (DMN-040); без license_ack репозиторий с LICENSE отвечает 409 + license_required, неоднозначный пакет — 409 + ambiguous (список кандидатов) — из них CLI строит диалог согласия / выбор источника |
GET /v1/apps/{id} | AppService.GetApp | Одно приложение |
GET /v1/apps/{id}/disk | AppService.GetAppDisk | Дисковое пространство: образ, репозиторий, данные, кастомные тома |
POST /v1/apps/{id}/start|stop|restart | AppService.Start/Stop/RestartApp | Жизненный цикл |
GET /v1/apps/{id}/logs?tail=N | AppService.GetAppLogs | Хвост логов |
DELETE /v1/apps/{id} | AppService.RemoveApp | Удаление с данными |
POST /v1/apps/{id}/console-token | AppService.IssueConsoleToken | Временный токен консоли |
GET /v1/metrics | MonitorService.GetSystemMetrics | Текущие системные метрики (503, пока нет первого сэмпла) |
GET /v1/metrics/history?limit=N | MonitorService.GetMetricsHistory | История метрик из кольцевого буфера, старые → новые |
📜 Кодогенерация
- Rust-код генерируется из
proto/в build.rs через protox (pure-Rust компилятор protobuf) + tonic-build — системныйprotocне нужен, сборка герметична. - Изменения контрактов — только обратно совместимые (новые поля — optional, номера не переиспользовать).
🔗 Связанные задачи
DMN-005, DMN-007, DMN-042, DMN-043 в ROADMAP.md.