Merge branch 'docs' into 'master'

Add example `.env` files and configuration documentation for `measurements`...

See merge request infra/iac!2
This commit is contained in:
Дмитрий Емелин 2026-07-15 13:44:56 +00:00
commit d00f91688a
133 changed files with 41088 additions and 0 deletions

View File

@ -0,0 +1,85 @@
# =============================================================================
# AMS-Sync — пример конфигурации (.env)
# Версия: 1.0.0
# Скопируйте в .env и заполните значения.
#
# Сервис имеет два режима запуска:
# - `run` — демон-потребитель Kafka (CMD контейнера), использует блоки
# App / Kafka / Zitadel / Logger / Healthcheck (+ Auth);
# - CLI — команды migrate / sync / diff (запускаются вручную),
# дополнительно используют блоки Auth / User server / БД sarex.
# Значения по умолчанию проставлены там, где они заданы в коде.
# =============================================================================
# --- Приложение (без префикса) ---
# LOCAL | STAGE | PREPROD | PRODUCTION
ENVIRONMENT=LOCAL
# Топик Kafka с событиями пользователей
AMS_SYNC_TOPIC=ams-sync
# ID организации-хоста в Zitadel (обязателен)
HOST_ORGANIZATION_ID=
# При True входящие model_created обрабатываются через import_user (с верификацией)
VERIFY_USERS=False
# --- Kafka (префикс KAFKA_) ---
# JSON-массив адресов брокеров, напр. ["host:9091"]
KAFKA_BOOTSTRAP_SERVERS=
KAFKA_SASL_PLAIN_USERNAME=
KAFKA_SASL_PLAIN_PASSWORD=
# Путь к CA-сертификату для SSL-подключения к Kafka
KAFKA_SSL_CAFILE=
# PLAINTEXT | SSL | SASL_PLAINTEXT | SASL_SSL
KAFKA_SECURITY_PROTOCOL=SASL_SSL
# PLAIN | SCRAM-SHA-256 | SCRAM-SHA-512
KAFKA_SASL_MECHANISM=SCRAM-SHA-512
# --- Zitadel / AMS (префикс ZITADEL_) ---
# Хост Zitadel (обязателен для адаптера пользователей)
ZITADEL_HOST=https://idp.dev.stage.sarex.io
# Service-токен доступа к API Zitadel (обязателен)
ZITADEL_SERVICE_ACCESS_TOKEN=
# ВНИМАНИЕ: переменную читают сразу несколько классов с разным типом
# - адаптер пользователей ожидает int (минуты), напр. 10
# - адаптеры организаций/грантов ожидают строку, напр. "10m"
# Задавайте int, иначе адаптер пользователей упадёт на разборе.
ZITADEL_TIMEOUT=10
# Максимальный размер пачки при bulk-импорте
ZITADEL_MAX_BATCH=2000
# Эндпоинты (обычно не переопределяются)
ZITADEL_USERS_MANAGEMENT_ENDPOINT=management/v1/users
ZITADEL_USERS_ENDPOINT=v2/users
# --- Логирование (префикс LOG_) ---
LOG_LEVEL=INFO
# LOG_FORMAT=%(asctime)s [%(levelname)s]: %(message)s
# --- TCP Healthcheck (префикс HEALTHCHECK_) ---
HEALTHCHECK_HOST=0.0.0.0
HEALTHCHECK_PORT=8008
HEALTHCHECK_MAX_CONNECTIONS=10
HEALTHCHECK_REPLY=healthy
# --- Auth: sarex-backend (префикс AUTH_) ---
# Используется для получения JWT под админом (режим run и CLI)
AUTH_HOST=https://stage.sarex.io
AUTH_ADMIN_USERNAME=
AUTH_ADMIN_PASSWORD=
AUTH_AUTH_ENDPOINT=api/token/
# --- Sarex user server (префикс USER_SERVER_, только CLI) ---
# Источник метадаты пользователя для миграции/синхронизации
USER_SERVER_HOST=https://stage.sarex.io
USER_SERVER_USERS_ENDPOINT=api/core/admin/users
# --- База данных sarex-backend (префикс USER_DB_, только CLI) ---
# Источник пользователей/компаний для команд migrate/sync/diff
USER_DB_NAME=postgres
USER_DB_USER=postgres
USER_DB_PASSWORD=password
USER_DB_HOST=127.0.0.1
USER_DB_PORT=5432
# Имена таблиц (обычно не переопределяются)
USER_DB_USER_TABLE_NAME=base_baseuser
USER_DB_COMPANYUSER_TABLE_NAME=core_companyuser
USER_DB_COMPANY_TABLE_NAME=core_company
USER_DB_ITER_SIZE=10000

View File

@ -0,0 +1,218 @@
# Конфигурация проекта ams-sync
# Версия: 1.0.0
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Что делает сервис
`ams-sync` — утилита синхронизации пользователей sarex-backend с системой управления доступом (AMS) на базе **Zitadel**. У сервиса два режима работы:
- **`run`** — фоновый демон-потребитель Kafka (`src/internal/app/service/sync_service.py`, точка входа `python3 cmd.py run`, это же `CMD` контейнера). Слушает топик событий пользователей и создаёт/обновляет пользователей в Zitadel. Параллельно поднимает TCP-healthcheck.
- **CLI-команды** (`migrate` / `sync` / `diff`, реализованы в `src/internal/app/console.py`) — разовые задачи миграции и синхронизации, запускаются вручную. Читают пользователей и компании напрямую из БД sarex-backend.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). В отличие от монолитного `AppSettings`, настройки разбиты на несколько независимых классов, каждый со своим префиксом:
| Класс | Файл | Префикс |
| --- | --- | --- |
| `Settings` (приложение) | `src/internal/app/service/settings.py` | *(без префикса)* |
| `KafkaSettings` | `src/internal/app/service/settings.py` | `kafka_` |
| `Settings` (console) | `src/internal/app/console.py` | *(без префикса)* |
| `Settings` (logger) | `src/internal/adapter/logger/logger.py` | `log_` |
| `Settings` (healthcheck) | `src/internal/adapter/tcphealthcheck/healthcheck.py` | `healthcheck_` |
| `Settings` (auth) | `src/internal/adapter/auth/http/adapter.py` | `auth_` |
| `Settings` (user server) | `src/internal/adapter/users/http/adapter.py` | `user_server_` |
| `Settings` (zitadel users) | `src/internal/adapter/users/zitadel/adapter.py` | `zitadel_` |
| `Settings` (organizations) | `src/internal/adapter/organizations/adapter.py` | `zitadel_` |
| `Settings` (grants) | `src/internal/adapter/grants/adapter.py` | `zitadel_` |
| `Settings` (user repo) | `src/internal/repository/users/repository.py` | `user_db_` |
| `Settings` (company repo) | `src/internal/repository/companies/repository.py` | `user_db_` |
Особенности разбора (`SettingsConfigDict`):
- у каждого класса задан `env_file=".env"`, `env_file_encoding="utf-8"` и `extra="ignore"` — при наличии файла `.env` в рабочей директории он загружается автоматически, лишние переменные игнорируются;
- вложенного разделителя нет — каждая группа настроек читается отдельным классом по своему префиксу (либо без префикса для классов приложения);
- CLI-команды принимают флаг `--env-file=` (по умолчанию `.env`), который прокидывается в конструкторы настроек как `_env_file`; режим `run` всегда читает `.env`.
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (CLI) | Файл `.env` (или указанный через `--env-file=`) в директории с `cmd.py` и/или переменные окружения процесса |
| Локально (контейнер) | `Dockerfile`: `CMD ["python3", "cmd.py", "run"]`; переменные пробрасываются в окружение контейнера |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `universal-chart.envs` (обычные значения) и `universal-chart.secretEnvs` (значения из k8s-секретов). Базовый чарт — `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна в `workflow.rules` (`RELEASE_NAME`, `STAND`, `CHART_*`, `IMAGE_NAME`, `HELM_SET_ARGS` и т.п.) |
Команды CLI (`python3 cmd.py <command>`, см. `README.md`):
| Команда | Аргументы | Назначение |
| --- | --- | --- |
| `run` | — | Демон-потребитель Kafka + TCP-healthcheck (режим контейнера) |
| `migrate` | `--user-id`, `--with-password/--no-with-password` | Миграция пользователя(ей) из БД sarex-backend в Zitadel. Без `--user-id` мигрируются все (bulk) |
| `sync` | `--user-id`, `--company-id` | Синхронизация метадаты пользователя(ей) из БД sarex-backend в Zitadel |
| `diff` | — | Сравнение множества пользователей БД и Zitadel, отчёт по отсутствующим |
## Переменные приложения
Дефолт `—` означает, что значение обязательно (иначе ошибка старта).
### App (без префикса)
Класс `Settings` в `src/internal/app/service/settings.py` (режим `run`) и `Settings` в `console.py` (CLI).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENVIRONMENT` | enum | `LOCAL` | Окружение: `LOCAL`/`STAGE`/`PREPROD`/`PRODUCTION` |
| `AMS_SYNC_TOPIC` | string | `ams-sync` | Имя топика Kafka с событиями пользователей |
| `HOST_ORGANIZATION_ID` | string | — | ID организации-хоста в Zitadel (обязателен и в `run`, и в CLI) |
| `VERIFY_USERS` | bool | `False` | При `True` события `model_created` обрабатываются через `import_user` (с верификацией e-mail), иначе через обычный `create` |
### Kafka (`KAFKA_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | — | Адреса брокеров, напр. `["host:9091"]` |
| `KAFKA_SASL_PLAIN_USERNAME` | string | — | Логин SASL |
| `KAFKA_SASL_PLAIN_PASSWORD` | string | — | Пароль SASL |
| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату для SSL |
| `KAFKA_SECURITY_PROTOCOL` | string | `SASL_SSL` | Протокол безопасности |
| `KAFKA_SASL_MECHANISM` | string | `SCRAM-SHA-512` | Механизм SASL |
### Zitadel / AMS (`ZITADEL_*`)
Префикс используют три класса (адаптеры пользователей, организаций и грантов). Общие поля:
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ZITADEL_SERVICE_ACCESS_TOKEN` | string | — | Service-токен доступа к API Zitadel (Bearer) |
| `ZITADEL_HOST` | string | — (у адаптера пользователей); `https://idp.dev.stage.sarex.io` (у адаптеров организаций/грантов) | Базовый URL Zitadel |
| `ZITADEL_TIMEOUT` | int / string | `10` (адаптер пользователей, минуты) / `10m` (организации, гранты) | Таймаут. См. замечание о конфликте типов ниже |
| `ZITADEL_MAX_BATCH` | int | `2000` | Максимум пользователей в одной пачке bulk-импорта |
| `ZITADEL_USERS_MANAGEMENT_ENDPOINT` | string | `management/v1/users` | Базовый путь management-API |
| `ZITADEL_USERS_ENDPOINT` | string | `v2/users` | Базовый путь users-API v2 |
### Логирование (`LOG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `INFO` | Уровень логирования |
| `LOG_FORMAT` | string | `%(asctime)s [%(levelname)s]: %(message)s` | Формат строки лога |
Логи пишутся в stdout и в файл `latest.log` в рабочей директории.
### TCP Healthcheck (`HEALTHCHECK_*`)
Только режим `run`. Поднимает сырой TCP-сокет, отвечающий строкой-ответом на каждое подключение.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HEALTHCHECK_HOST` | string | `0.0.0.0` | Адрес прослушивания |
| `HEALTHCHECK_PORT` | int | `8008` | Порт |
| `HEALTHCHECK_MAX_CONNECTIONS` | int | `10` | Размер очереди подключений (`listen`) |
| `HEALTHCHECK_REPLY` | string | `healthy` | Строка-ответ на подключение |
### Auth: sarex-backend (`AUTH_*`)
Получение JWT под сервисным админом (используется в обоих режимах — адаптер создаётся и в `run`, и в CLI).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AUTH_HOST` | string | — | Базовый URL sarex-backend |
| `AUTH_ADMIN_USERNAME` | string | — | Логин админа |
| `AUTH_ADMIN_PASSWORD` | string | — | Пароль админа |
| `AUTH_AUTH_ENDPOINT` | string | `api/token/` | Путь получения токена |
### Sarex user server (`USER_SERVER_*`) — только CLI
Источник метадаты пользователя при миграции/синхронизации.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USER_SERVER_HOST` | string | — | Базовый URL сервиса пользователей sarex-backend |
| `USER_SERVER_USERS_ENDPOINT` | string | `api/core/admin/users` | Базовый путь эндпоинта пользователей |
### База данных sarex-backend (`USER_DB_*`) — только CLI
Один префикс на два репозитория (пользователи и компании). Подключение через `psycopg2`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USER_DB_NAME` | string | — | Имя базы данных |
| `USER_DB_USER` | string | — | Пользователь БД |
| `USER_DB_PASSWORD` | string | — | Пароль |
| `USER_DB_HOST` | string | — | Хост PostgreSQL |
| `USER_DB_PORT` | int | — | Порт PostgreSQL |
| `USER_DB_USER_TABLE_NAME` | string | `base_baseuser` | Таблица пользователей |
| `USER_DB_COMPANYUSER_TABLE_NAME` | string | `core_companyuser` | Таблица связи пользователь↔компания |
| `USER_DB_COMPANY_TABLE_NAME` | string | `core_company` | Таблица компаний |
| `USER_DB_ITER_SIZE` | int | `10000` | Размер серверного курсора при итерации |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Базовый чарт — `universal-chart`. Обычные значения (`universal-chart.envs`) задаются на окружение (`_default`/`stage`/`preprod`/`production`):
| Переменная | Значения по окружениям |
| --- | --- |
| `AMS_SYNC_TOPIC` | `ams-sync` |
| `ENVIRONMENT` | `STAGE` / `PREPROD` / `PRODUCTION` |
| `HOST_ORGANIZATION_ID` | stage `339439562105337368`, preprod `337394329179947547`, production `337555824748561429` |
| `USER_SERVER_HOST` | stage `https://stage.sarex.io`, preprod `https://preprod.sarex.io`, production `https://.lk.sarex.io` |
| `AUTH_HOST` | stage `https://stage.sarex.io`, preprod `https://preprod.sarex.io`, production `https://lk.sarex.io` |
| `KAFKA_SECURITY_PROTOCOL` | `SASL_SSL` |
| `KAFKA_SASL_MECHANISM` | `SCRAM-SHA-512` |
Значения из секретов (`universal-chart.secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `ZITADEL_HOST` | `zitadel-token-secret` | `zitadel-host` |
| `ZITADEL_SERVICE_ACCESS_TOKEN` | `zitadel-token-secret` | `zitadel-access-token` |
| `KAFKA_BOOTSTRAP_SERVERS` | `ams-kafka-secret` | `kafka-bootstrap-servers` |
| `KAFKA_SASL_PLAIN_USERNAME` | `ams-kafka-secret` | `kafka-username` |
| `KAFKA_SASL_PLAIN_PASSWORD` | `ams-kafka-secret` | `kafka-password` |
| `KAFKA_SSL_CAFILE` | `ams-kafka-secret` | `kafka-ca-file` |
| `AUTH_ADMIN_USERNAME` | `auth-secret` | `admin-username` |
| `AUTH_ADMIN_PASSWORD` | `auth-secret` | `admin-password` |
| `USER_DB_NAME` | `user-db-secret` | `user-db-name` |
| `USER_DB_USER` | `user-db-secret` | `user-db-user` |
| `USER_DB_PASSWORD` | `user-db-secret` | `user-db-password` |
| `USER_DB_HOST` | `user-db-secret` | `user-db-host` |
| `USER_DB_PORT` | `user-db-secret` | `user-db-port` |
Помимо env, чарт монтирует CA-сертификат Kafka из секрета `ya-ca-secret` в `/etc/ca-certificates/Yandex` (`volumes`/`volumeMounts`) и раздаёт `YandexInternalRootCA.crt` через configMap `ya-ca-cert`. Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, порт `8008`), `service.*` (ClusterIP, порт `8008`), `probes.*` (TCP-пробы на `8008`, по умолчанию выключены), `serviceAccount.*`, `imagePullSecrets.*`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace |
| --- | --- | --- |
| ветка `stage` | `stage` | `platform` (`STAGE_NAMESPACE`) |
| ветка `master` | `preprod` | `ams-sync-preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `ams-sync-prod` |
Ключевые переменные пайплайна: `RELEASE_NAME=ams-sync`, `CHART_NAME=ams-sync`, `CHART_VERSION` (`0.0.1-<stand>`), `IMAGE_NAME`, `IMAGE_PATH=universal-chart.image.name`, `HELM_SET_ARGS`, `DOCKERFILE_PATH=Dockerfile`, флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY`.
## Замечания и потенциальные проблемы
- **Конфликт `ZITADEL_TIMEOUT`.** Переменную читают три класса с общим префиксом `zitadel_`, но разного типа: адаптер пользователей ожидает `int` (минуты, дефолт `10`), адаптеры организаций и грантов — `str` (дефолт `10m`). Если задать `ZITADEL_TIMEOUT=10m`, инициализация адаптера пользователей упадёт на разборе `int`. Рекомендуется задавать целое число.
- **`ZITADEL_HOST` без дефолта у адаптера пользователей.** В `users/zitadel/adapter.py` поле `host` обязательно (дефолта нет), тогда как в адаптерах организаций/грантов есть дефолт `https://idp.dev.stage.sarex.io`. Для режима `run` переменную нужно задать явно (в Helm приходит из секрета `zitadel-token-secret`).
- **Режим `run` не использует БД и user-server.** Демон-потребитель (`sync_service.py`) поднимает только logger, healthcheck, auth-провайдер, адаптер Zitadel и Kafka. Переменные `USER_DB_*` и `USER_SERVER_*` требуются только CLI-командам (`migrate`/`sync`/`diff`), хотя в Helm они проброшены для запуска этих команд через `kubectl exec`.
- **Приложение загружает `.env` автоматически** (у всех классов задан `env_file`), в отличие от некоторых других сервисов. CLI дополнительно поддерживает `--env-file=` для альтернативного файла.
- **TLS-проверка отключена.** Все исходящие HTTP-запросы (Zitadel, sarex-backend) выполняются с `verify=False`. Отдельного флага в конфиге нет.
- **Zitadel org-ID захардкожены в коде.** Помимо `HOST_ORGANIZATION_ID`, в use-case зашиты `glorax_org_id` и `dogma_org_id`, в которые распределяются пользователи по доменам e-mail (см. `usecase/migration/usecase.py`, `usecase/user/usecase.py`).
## Минимальный набор для режима `run` (демон)
- `ENVIRONMENT`, `HOST_ORGANIZATION_ID`, `AMS_SYNC_TOPIC`
- `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_SASL_PLAIN_USERNAME`, `KAFKA_SASL_PLAIN_PASSWORD`, `KAFKA_SSL_CAFILE` (+ при необходимости `KAFKA_SECURITY_PROTOCOL`, `KAFKA_SASL_MECHANISM`)
- `ZITADEL_HOST`, `ZITADEL_SERVICE_ACCESS_TOKEN`
- `AUTH_HOST`, `AUTH_ADMIN_USERNAME`, `AUTH_ADMIN_PASSWORD`
## Дополнительно для CLI (`migrate` / `sync` / `diff`)
- `USER_SERVER_HOST`
- `USER_DB_NAME`, `USER_DB_USER`, `USER_DB_PASSWORD`, `USER_DB_HOST`, `USER_DB_PORT`

View File

@ -0,0 +1,99 @@
# Интерфейсы сервиса ams-sync
# Версия: 1.0.0
Документ описывает интерфейсную поверхность сервиса: потребляемый топик Kafka, входящий TCP-healthcheck, исходящие HTTP-запросы к внешним сервисам (Zitadel, sarex-backend) и обращения к БД.
> `ams-sync` — не backend-сервис с REST API, а утилита синхронизации: демон-потребитель Kafka (`run`) плюс набор CLI-команд (`migrate`/`sync`/`diff`). Публичного HTTP API у сервиса нет — единственный входящий сокет отвечает на healthcheck сырой строкой (не HTTP). Поэтому файла `openapi.yaml` для сервиса нет.
## Как устроено взаимодействие
Приложение построено по слоям (adapter → repository → usecase → controller). Исходящие вызовы к внешним сервисам выполняются через `requests` (`requests.Session` с ретраями на `5xx`), к БД — через `psycopg2`. Все HTTP-запросы идут с `verify=False` (без проверки TLS). Базовые хосты берутся из переменных окружения (см. `CONFIGURATION.md`): `ZITADEL_HOST`, `AUTH_HOST`, `USER_SERVER_HOST`.
Аутентификация исходящих запросов:
- **Zitadel** — заголовок `Authorization: Bearer <ZITADEL_SERVICE_ACCESS_TOKEN>` (service-токен из конфига). Для части операций дополнительно проставляется `x-zitadel-orgid`.
- **sarex-backend** — JWT, получаемый под админом (`AUTH_ADMIN_USERNAME`/`AUTH_ADMIN_PASSWORD`) через `POST api/token/`; кешируется и обновляется за 30 секунд до `exp`.
## Входящие интерфейсы
### TCP Healthcheck
Только режим `run`. Сырой TCP-сокет (не HTTP): на каждое подключение отправляет строку `HEALTHCHECK_REPLY` (по умолчанию `healthy`) и закрывает соединение. Слушает `HEALTHCHECK_HOST:HEALTHCHECK_PORT` (по умолчанию `0.0.0.0:8008`). В Helm k8s-пробы настроены на TCP-порт `8008` (по умолчанию отключены).
### Kafka-потребитель
Топик — переменная `AMS_SYNC_TOPIC` (по умолчанию `ams-sync`). Параметры консьюмера (`src/internal/controller/sync_service/controller.py`):
| Параметр | Значение |
| --- | --- |
| `group_id` | `ams_sync_group` |
| `auto_offset_reset` | `earliest` |
| `enable_auto_commit` | `True` |
| десериализация | JSON (`utf-8`) |
Формат сообщения — объект с полями `type` и `body`. Диспетчеризация по `type`:
| `type` сообщения | Тело (`body`) | Обработчик | Действие |
| --- | --- | --- | --- |
| `model_created` | `SarexUserWithPermissions` | при `VERIFY_USERS=True``import_user` UC, иначе `user` UC `create` | Создание пользователя в Zitadel + запись метадаты |
| `model_updated` | `Metadata` | `user` UC `update_metadata` | Обновление метадаты существующего пользователя в Zitadel |
Сообщения без `type` или `body` пропускаются с предупреждением. Ошибки обработки логируются (traceback), сообщение не переотправляется (auto-commit включён).
## Исходящие HTTP-запросы
### Zitadel (`ZITADEL_HOST`)
Адаптеры `users/zitadel/adapter.py` и `organizations/adapter.py`. Итоговый URL = `<ZITADEL_HOST>` + путь.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/v2/users/new` | Создать пользователя (human) |
| POST | `/management/v1/users/human/_import` | Импортировать пользователя (заголовок `x-zitadel-orgid`) |
| POST | `/admin/v1/import` | Массовый импорт пользователей (bulk, с таймаутом) |
| POST | `/v2/users` | Поиск пользователей (по username / email / org) |
| GET | `/management/v1/users/{id}` | Получить пользователя по id |
| GET | `/management/v1/users/{id}/metadata/{key}` | Получить метадату пользователя по ключу |
| POST | `/v2/users/{id}/metadata` | Массово задать метадату пользователя (значения в base64) |
| POST | `/v2/users/{id}/deactivate` | Деактивировать пользователя |
| DELETE | `/v2/users/{id}` | Удалить пользователя |
| POST | `/v2/organizations/_search` | Найти организацию по имени |
> Пути `/v2/...` частично захардкожены в адаптере, часть базовых путей управляется `ZITADEL_USERS_MANAGEMENT_ENDPOINT` (`management/v1/users`) и `ZITADEL_USERS_ENDPOINT` (`v2/users`). Методы грантов (`grants/adapter.py`) в текущей версии закомментированы.
### sarex-backend — авторизация (`AUTH_HOST`)
Адаптер `auth/http/adapter.py`.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{AUTH_AUTH_ENDPOINT}` (по умолчанию `api/token/`) | Получить JWT по логину/паролю админа. Возвращает поле `access` |
### sarex-backend — сервис пользователей (`USER_SERVER_HOST`), только CLI
Адаптер `users/http/adapter.py`. Сессия с ретраями на `500/502/503/504` (до 5 попыток).
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `{USER_SERVER_USERS_ENDPOINT}/{user_id}/metadata` (по умолчанию `api/core/admin/users/{user_id}/metadata`) | Получить метадату пользователя sarex для синхронизации в Zitadel |
## Обращения к БД sarex-backend (`USER_DB_*`), только CLI
Репозитории `repository/users` и `repository/companies` читают данные напрямую из PostgreSQL sarex-backend (только чтение, серверный курсор `itersize`).
| Операция | Таблицы | Назначение |
| --- | --- | --- |
| Пользователь по id | `base_baseuser` | `migrate --user-id` / `sync --user-id` |
| Все пользователи (итерация) | `base_baseuser` | `migrate` / `sync` / `diff` (bulk) |
| Пользователи компании | `core_companyuser``base_baseuser` | `sync --company-id` (только с непустым email) |
| Компания по id / имени | `core_company` | Вспомогательные запросы |
## Внешние зависимости (инфраструктура)
| Зависимость | Режим | Назначение |
| --- | --- | --- |
| Kafka | `run` | Источник событий пользователей (топик `AMS_SYNC_TOPIC`) |
| Zitadel (AMS) | `run` + CLI | Целевая система управления доступом (создание/обновление/удаление пользователей и метадаты) |
| sarex-backend (auth) | `run` + CLI | Получение JWT под админом |
| sarex-backend (user server) | CLI | Источник метадаты пользователя |
| PostgreSQL sarex-backend | CLI | Источник пользователей и компаний для миграции/синхронизации |

View File

@ -0,0 +1,46 @@
# =============================================================================
# Attachments — пример конфигурации (.env)
# Скопируйте в .env и заполните значения.
# =============================================================================
# --- Приложение (необязательные, есть значения по умолчанию) ---
# Версия: 0.11.1
# API=/api
# NAME=Attachments
# VERSION=0.0.1
# DESCRIPTION=Attachments
# --- База данных PostgreSQL (обязательные) ---
DATABASE_NAME=attachments
DATABASE_USER=postgres
DATABASE_PASSWORD=change_me
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_SSL_MODE=disable
# Пароль суперпользователя PostgreSQL для контейнера db (docker-compose)
POSTGRES_PASSWORD=change_me
# --- S3 (Yandex Object Storage) ---
# Вариант 1: путь к JSON с реквизитами сервисного аккаунта.
# Файл должен содержать: {"endpoint": "...", "access_key_id": "...", "secret_access_key": "..."}
YANDEX_S3_ACCOUNT_PATH=/etc/sarex/yc-s3-storage/yc-s3-service-account.json
# Вариант 2: задать реквизиты напрямую (если не используете JSON-файл).
# YANDEX_S3_ENDPOINT_URL=storage.yandexcloud.net
# YANDEX_S3_ACCESS_KEY_ID=change_me
# YANDEX_S3_SECRET_ACCESS_KEY=change_me
YANDEX_S3_VERIFY=true
# YANDEX_S3_USE_SSL=true
# YANDEX_S3_REGION=ru-central1
BUCKET_NAME=attachments-stage-2
# --- Логирование (префикс LOG_) ---
# LOG_LEVEL=INFO
# --- Трейсинг / OpenTelemetry (префикс TRACING_) ---
# TRACING_USE=false
# TRACING_HOST=localhost:4317
# TRACING_SERVICE_NAME=attachments
# TRACING_INSECURE=false

View File

@ -0,0 +1,129 @@
# Конфигурация проекта Attachments
# Версия: 0.11.1
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Настройка сервиса выполняется **только через переменные окружения**. Отдельного файла с настройками (yaml/toml) в приложении нет — за конфигурацию отвечает `internal/config/settings.py` на базе `pydantic.BaseSettings`.
Источники переменных окружения по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (docker-compose) | Файл `.env` в корне (`env_file: .env` в `docker-compose.yml`), плюс `POSTGRES_PASSWORD` для контейнера БД |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна и build-args |
Дополнительно секреты доступа к S3 не задаются напрямую, а читаются из JSON-файла (см. `YANDEX_S3_ACCOUNT_PATH` ниже).
## Переменные приложения (класс `Settings`)
Читаются напрямую по имени (регистрозависимо, `case_sensitive = True`). Переменные без значения по умолчанию **обязательны** — без них приложение не стартует.
| Переменная | Тип | Обязательна | Значение по умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `API` | str | нет | `/api` | Префикс всех HTTP-роутов |
| `NAME` | str | нет | `Attachments` | Имя сервиса (title в FastAPI/OpenAPI) |
| `VERSION` | str | нет | `0.0.1` | Версия сервиса |
| `DESCRIPTION` | str | нет | `Attachments` | Описание сервиса |
| `YANDEX_S3_ENDPOINT_URL` | str | да* | — | Endpoint S3 (без схемы; `https://`/`http://` добавляется в коде по `YANDEX_S3_USE_SSL`) |
| `YANDEX_S3_ACCESS_KEY_ID` | str | да* | — | Access Key ID для S3 |
| `YANDEX_S3_SECRET_ACCESS_KEY` | str | да* | — | Secret Access Key для S3 |
| `YANDEX_S3_USE_SSL` | bool | нет | `True` | Использовать ли HTTPS при обращении к S3 |
| `YANDEX_S3_REGION` | str | нет | `ru-central1` | Регион S3 |
| `YANDEX_S3_VERIFY` | bool | да | — | Проверять ли SSL-сертификат S3 |
| `BUCKET_NAME` | str | да | — | Имя бакета для вложений |
| `DATABASE_NAME` | str | да | — | Имя базы данных PostgreSQL |
| `DATABASE_USER` | str | да | — | Пользователь БД |
| `DATABASE_PASSWORD` | str | да | — | Пароль пользователя БД |
| `DATABASE_HOST` | str | да | — | Хост БД |
| `DATABASE_PORT` | int | да | — | Порт БД |
| `DATABASE_SSL_MODE` | str | да | — | Режим SSL при подключении к БД (напр. `disable`, `require`, `verify-full`) |
\* Три переменные `YANDEX_S3_ENDPOINT_URL`, `YANDEX_S3_ACCESS_KEY_ID`, `YANDEX_S3_SECRET_ACCESS_KEY` формально обязательны, но при старте приложения они **проставляются автоматически** функцией `json_config_s3_account_settings()` из JSON-файла (см. `YANDEX_S3_ACCOUNT_PATH`). Задавать их вручную не нужно, если задан путь к файлу.
## Настройки S3 через JSON-файл
| Переменная | Обязательна | Назначение |
| --- | --- | --- |
| `YANDEX_S3_ACCOUNT_PATH` | да | Путь к JSON-файлу с реквизитами сервисного аккаунта S3 |
При старте функция `json_config_s3_account_settings()` читает файл по этому пути и выставляет переменные окружения `YANDEX_S3_ENDPOINT_URL`, `YANDEX_S3_ACCESS_KEY_ID`, `YANDEX_S3_SECRET_ACCESS_KEY`.
Ожидаемая структура JSON-файла:
```json
{
"endpoint": "storage.yandexcloud.net",
"access_key_id": "<ключ>",
"secret_access_key": "<секрет>"
}
```
В Kubernetes файл монтируется из секрета `attachments-s3-secret` (в prod — `yc-s3`) в `/etc/sarex/yc-s3-storage/yc-s3-service-account.json`.
## Логирование (класс `LoggerSettings`, префикс `LOG_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | str | `INFO` | Уровень логирования (`DEBUG`, `INFO`, `WARNING`, ...). При неизвестном значении откатывается на `INFO` |
| `LOG_FORMAT` | str | JSON-шаблон с полями `timestamp`, `level`, `message` | Формат строк лога (используется `pythonjsonlogger`) |
## Трейсинг / OpenTelemetry (класс `TraceSettings`, префикс `TRACING_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRACING_USE` | bool | `False` | Включает OTEL-трейсинг, логгер и middleware |
| `TRACING_HOST` | str | `localhost:4317` | Адрес OTLP-коллектора |
| `TRACING_SERVICE_NAME` | str | `attachments` | Имя сервиса в трейсах |
| `TRACING_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение к коллектору |
Трейсинг активируется только при `TRACING_USE=true`.
## Переменные инфраструктуры и сборки
Не читаются кодом приложения, но нужны для запуска/сборки/деплоя.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `POSTGRES_PASSWORD` | `docker-compose.yml` (контейнер `db`) | Пароль суперпользователя PostgreSQL при локальном запуске |
| `PIP_EXTRA_INDEX_URL` | `docker/Dockerfile` (build-arg) | Доп. индекс pip для установки приватных пакетов при сборке образа |
| `GITLAB_PYPI_EXTRA_INDEX_URL` | `.gitlab-ci.yml` | Значение, пробрасываемое в `PIP_EXTRA_INDEX_URL` при сборке в CI |
### Переменные CI/CD (`.gitlab-ci.yml`)
Служебные переменные пайплайна: `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE`, а также подставляемые по окружениям `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`. Окружение выбирается по ветке/тегу: `stage` → ветка `stage`, `preprod` → ветка `master`, `production` → git-тег.
## Переменные из Helm-чарта (`.helm/values.yaml`)
Обычные переменные (`envs`):
| Переменная | Значение | Примечание |
| --- | --- | --- |
| `API_ADDRESS` | `0.0.0.0:8000` | Задаётся в чарте, но **кодом приложения не читается** |
| `POSTGRES_POOL_SIZE` | `10` | Задаётся в чарте, но **кодом приложения не читается** |
| `DATABASE_SSL_MODE` | `verify-full` | |
| `YANDEX_S3_VERIFY` | `true` | |
| `YANDEX_S3_ACCOUNT_PATH` | `/etc/sarex/yc-s3-storage/yc-s3-service-account.json` | |
| `BUCKET_NAME` | `attachments-stage-2` / `attachments-prod-2` | Зависит от окружения |
Переменные из секретов (`secretEnvs`, секрет `attachments-postgresql-secret` / `ya-pg-secret`):
| Переменная | Ключ секрета | Примечание |
| --- | --- | --- |
| `DATABASE_PORT` | `port` | |
| `DATABASE_HOST` | `host` | |
| `DATABASE_USER` | `username` | |
| `DATABASE_PASSWORD` | `password` | |
| `DATABASE_NAME` | `database` | |
| `YC-PG-CERTIFICATE` | `ca.crt` | CA-сертификат PostgreSQL; также монтируется файлом в `/root/.postgresql/root.crt` |
## Минимальный набор для локального запуска
Для запуска через `docker-compose` в файле `.env` достаточно задать (см. `.env.example`):
- `POSTGRES_PASSWORD` — для контейнера БД
- `DATABASE_*` — параметры подключения к БД
- `BUCKET_NAME`, `YANDEX_S3_VERIFY`
- `YANDEX_S3_ACCOUNT_PATH` **или** напрямую `YANDEX_S3_ENDPOINT_URL` + `YANDEX_S3_ACCESS_KEY_ID` + `YANDEX_S3_SECRET_ACCESS_KEY`

View File

@ -0,0 +1,15 @@
# auth-flow-frontend — переменные СБОРКИ (build-time), НЕ рантайм-.env.
# Приложение не читает .env: значения подставляются в код на этапе сборки
# через webpack DefinePlugin (см. webpack.config.ts) или Dockerfile --build-arg / CI BUILD_ARGS.
# Окружение сборки: local | stage | prod | preprod | contour
# local -> mode=development, isDev=true (доступны dev-роуты /login, /logout, /)
# остальные -> mode=production
ENDPOINT=local
# Токен приватного npm-реестра @sarex-team (nexus.infra.sarex.io), нужен для `npm i` (.npmrc)
NPM_NEXUS_TOKEN=
# NOTE: IS_DEV вычисляется автоматически (ENDPOINT === 'local'), задавать вручную не нужно.
# NOTE: authority и client_id OIDC (Zitadel) НЕ задаются здесь — хостовое приложение
# кладёт их в localStorage (STORAGE.AUTHORITY / STORAGE.CLIENT_ID) во время работы.

View File

@ -0,0 +1,162 @@
# Конфигурация проекта auth-flow-frontend
Документ описывает способы конфигурирования микрофронтенда аутентификации `auth-flow-frontend` (React + webpack), а также все переменные сборки, деплоя и рантайма.
## Способы конфигурирования
В отличие от бэкенд-сервисов, приложение **не читает `.env` и не использует dotenv**. Это статический фронтенд, собираемый webpack, поэтому конфигурация распределена по трём уровням:
1. **Сборка (build-time).** Переменная `ENDPOINT` определяет окружение. Её значение подставляется в код на этапе сборки через `webpack.DefinePlugin` (`webpack.config.ts`) — заменяет обращения `process.env.ENDPOINT` и `process.env.IS_DEV` на строковые литералы. Выбор режима (`development`/`production`) и флага `isDev` выполняется в `webpack/build.config.ts`.
2. **Установка зависимостей.** Пакеты `@sarex-team/*` тянутся из приватного npm-реестра (`.npmrc` → `nexus.infra.sarex.io`). Для доступа нужен токен `NPM_NEXUS_TOKEN`.
3. **Рантайм (runtime).** Параметры OIDC-провайдера — `authority` и `client_id` — читаются из `localStorage` (`src/config/zitadel.ts`) по ключам `STORAGE.AUTHORITY` и `STORAGE.CLIENT_ID` из `@sarex-team/sdk-js`. Их задаёт хостовое приложение, а не сборка.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (dev) | `npm run dev``ENDPOINT=local webpack serve`. Dev-сервер на `https://localhost:9000` |
| Локально (build) | `npm run build``webpack --config webpack.config.ts` (по умолчанию `ENDPOINT=prod`, см. `build.config.ts`) |
| Docker | `Dockerfile`: build-args `ENDPOINT` и `NPM_NEXUS_TOKEN`; сборка dist → отдача через nginx |
| Kubernetes (Helm) | `.helm/values.yaml` (чарт `universal-chart`): блок `services.frontend`, `global.env` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` (ветка/тег → `STAND`, `ENDPOINT`, `CHART_VERSION`), `BUILD_ARGS`, `HELM_SET_ARGS` |
## Переменные сборки (build-time)
Подставляются в код на этапе сборки; в рантайме это уже константы.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENDPOINT` | enum | `prod``build.config.ts`, если не задана) | Окружение сборки: `local`/`stage`/`prod`/`preprod`/`contour`. Определяет `mode` (`development` для `local`, иначе `production`) и `isDev` |
| `IS_DEV` | bool | — (вычисляется) | `true`, если `ENDPOINT === 'local'`. Включает dev-роуты (`/login`, `/logout`, `/`). Задаётся автоматически через `DefinePlugin`, вручную указывать не нужно |
| `NPM_NEXUS_TOKEN` | string | — | Токен авторизации в приватном npm-реестре `@sarex-team` (`.npmrc`). Обязателен для `npm i` |
> `mode` по окружениям (`webpack/build.config.ts`): `local``development`, `stage`/`prod`/`preprod`/`contour` → `production`. Неизвестное значение `ENDPOINT` приводит к ошибке сборки.
## Переменные рантайма (localStorage)
Читаются в браузере во время работы приложения; задаются хостовым приложением, не сборкой.
| Ключ | Источник | Назначение |
| --- | --- | --- |
| `STORAGE.AUTHORITY` | `@sarex-team/sdk-js` | URL issuer'а Zitadel (`authority` OIDC). Обязателен — при отсутствии `src/config/zitadel.ts` бросает `Error("Authority or client ID is not set")` |
| `STORAGE.CLIENT_ID` | `@sarex-team/sdk-js` | `client_id` OIDC-клиента. Обязателен (та же проверка) |
| `sarex_logout` (`LOGOUT_STORAGE_KEY`) | `src/config/logout.ts` | Состояние логаута между вкладками (`{ logoutAt, owner, reason }`). Снимается только валидным токеном в `setTokensInfo` |
| access / refresh / id токены | `storage` из `@sarex-team/sdk-js` | Устанавливаются в `setTokensInfo` (`storage.setAccessToken`/`setRefreshToken`/`setIdentityToken`) после успешного колбэка |
## Конфигурация OIDC (Zitadel)
Задаётся в `src/config/zitadel.ts` (`configZitadel: ZitadelConfig`), клиент создаётся через `createZitadelAuth`.
| Параметр | Значение | Назначение |
| --- | --- | --- |
| `authority` | из `localStorage` (`STORAGE.AUTHORITY`) | Issuer Zitadel |
| `client_id` | из `localStorage` (`STORAGE.CLIENT_ID`) | Идентификатор клиента |
| `response_type` | `code` | Authorization Code Flow |
| `scope` | `openid profile email offline_access urn:zitadel:iam:user:metadata` | Запрашиваемые области |
| `redirect_uri` | `${window.location.origin}/auth/callback` | URL возврата после логина |
| `post_logout_redirect_uri` | `${window.location.origin}/login` | URL после логаута |
| `redirectMethod` | `replace` | Замена записи в истории браузера |
| `revokeTokensOnSignout` | `false` | Не отзывать токены при выходе |
| `automaticSilentRenew` | `false` | Фоновое обновление токенов выключено |
| `validateSubOnSilentRenew` | `false` | — |
| `silentRequestTimeoutInSeconds` | `100` | Таймаут silent-запроса |
## Роуты приложения
Определены в `src/config/urls.ts`, диспетчеризация в `src/App.tsx`.
| Роут | Константа | Доступность | Назначение |
| --- | --- | --- | --- |
| `/auth/callback` | `CALLBACK` | всегда | Обработка OIDC-колбэка (по умолчанию для неизвестных путей) |
| `/auth/error` | `ERROR` | всегда | Страница ошибки аутентификации |
| `/login` | `LOGIN` | только dev | Dev-страница входа |
| `/logout` | `LOGOUT` | только dev | Dev-страница выхода |
| `/` | `HOME` | только dev | Dev-навигация |
> В production разрешены только `/auth/callback` и `/auth/error` (`ALLOWED_PATHS`); в dev дополнительно `/login`, `/logout`, `/` (`ALLOWED_PATHS_DEV`). Неразрешённый путь заменяется на `/auth/callback` (`history.replaceState`).
## Сборка (webpack)
`webpack.config.ts` + `webpack/build.config.ts`.
| Параметр | Значение | Примечание |
| --- | --- | --- |
| `entry` | `src/index.tsx` | Точка входа |
| `output.path` | `dist/` | |
| `output.filename` | `index.js` | |
| `output.publicPath` | `/` (dev) или `/auth/callback/` (prod) | Зависит от `isDev` |
| loader | `esbuild-loader`, target `es2015` | Для `.tsx?/.jsx?` |
| `devServer` | `https`, `port: 9000`, `hot`, `historyApiFallback`, `open` | Только dev |
| plugins | `HtmlWebpackPlugin` (`public/index.html`), `DefinePlugin` (`ENDPOINT`, `IS_DEV`) | |
Node-версия для разработки: `v20.0.0` (`.nvmrc`; в `package.json` заявлено `v20.0.0`, реальная база образа — `node:20`).
## Docker
`Dockerfile` — двухстадийная сборка.
| Стадия | База | Действия |
| --- | --- | --- |
| build | `cr.yandex/crp3ccidau046kdj8g9q/node:20` | `npm i` (с `NPM_NEXUS_TOKEN`), `npm run build` (с `ENDPOINT`) |
| runtime | `cr.yandex/crp3ccidau046kdj8g9q/nginx:latest` | Копирует `/app/dist/``/dist/`, `nginx/nginx.conf``/etc/nginx/nginx.conf` |
Build-args: `ENDPOINT`, `NPM_NEXUS_TOKEN`.
## Nginx
`nginx/nginx.conf` — отдача статики и healthcheck.
| Локация | Поведение |
| --- | --- |
| `= /auth/callback/index.js` | `alias /dist/index.js` |
| `^~ /auth/callback` | `try_files $uri $uri/ /index.html`; заголовки `Cache-Control: no-store,...`, кеш отключён |
| `= /ping` | `200 {"result": "ok"}` (healthcheck) |
`listen 80`, `root /dist`, логи в stdout/stderr, `gzip on`.
## Helm-чарт (`.helm/values.yaml`)
Деплой через зонтичный чарт `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`; `Chart.yaml` приложения — `auth-flow-frontend` `1.0.0`). Значения задаются с ключами по окружениям (`_default`/`stage`/`preprod`/`production`).
| Параметр | Значение | Назначение |
| --- | --- | --- |
| `global.env` | `stage` (`_default` в файле) | Активное окружение чарта |
| `services.frontend.enabled` | `true` | Включение сервиса |
| `deployment.name._default` | `auth-flow-frontend` | Имя деплоймента |
| `deployment.replicaCount` | `_default: 1`, `production: 2` | Число реплик |
| `deployment.port._default` | `80` | Порт контейнера |
| `deployment.revisionHistoryLimit._default` | `5` | Хранимые ревизии |
| `deployment.resources.requests` | `memory: 128Mi`, `cpu: 100m` | Реквесты ресурсов |
| `deployment.probes.liveness/readiness` | `httpGet` `/` : `80` | Пробы |
| `image.name._default` | `cr.yandex/crp3ccidau046kdj8g9q/auth-flow-frontend` | Образ |
| `image.pullPolicy._default` | `IfNotPresent` | |
| `imagePullSecrets.name._default` | `dockerhub` (enabled `false`) | Секрет реестра |
| `service` | `ClusterIP`, port/targetPort `80`, portName `http`, имя `auth-flow-frontend-service` | Service |
| `envs` | `[]` | Переменные окружения контейнера (пусто) |
| `volumes._default` | `[]` | Тома |
| `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner` | заполняются из CI (`owner` по умолчанию `team-abc`) | Метаданные |
## CI (`.gitlab-ci.yml`)
Подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`).
Базовые переменные: `SERVICE_NAME=auth-flow-frontend`, `DOCKERFILE_PATH=./Dockerfile`, `CI_TRIGGER_SOURCE=app`.
Переключение окружения по `workflow.rules`:
| Условие | STAND | ENDPOINT | CHART_VERSION | K8S_HUSTLER_BRANCH |
| --- | --- | --- | --- | --- |
| `merge_request_event` | — | — | — (`ENABLE_BUILD_IMAGE=false`) | — |
| ветка `stage` | `stage` | `stage` | `0.0.1-stage` | `universal-chart-stage` |
| тег (`CI_COMMIT_TAG`) | `production` | `prod` | `0.0.1-prod` | `universal-chart-production` |
> Правило для ветки `master``preprod` присутствует, но **закомментировано**. `NAMESPACE` для всех окружений — `platform`, `RELEASE_NAME`/`CHART_NAME` — `${SERVICE_NAME}`.
`BUILD_ARGS` прокидывают `--build-arg ENDPOINT=<env>` и `--build-arg NPM_NEXUS_TOKEN=${NPM_NEXUS_TOKEN}`. `HELM_SET_ARGS` устанавливают `universal-chart.services.frontend.image.name.<env>`, `global.env`, а также `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`.
## Замечания и потенциальные проблемы
- **Обязательные runtime-параметры.** Если в `localStorage` нет `AUTHORITY` или `CLIENT_ID`, `src/config/zitadel.ts` бросает ошибку при инициализации — приложение не стартует. Эти значения должен положить хостовый сервис до загрузки микрофронтенда.
- **Нет `.env`.** Приложение не читает dotenv; `process.env.ENDPOINT`/`IS_DEV` существуют только на этапе сборки (замена через `DefinePlugin`). Значение `ENDPOINT` задаётся только через CLI/`--build-arg`/CI.
- **publicPath в prod**`/auth/callback/`: приложение обслуживается nginx под префиксом `/auth/callback`, что согласовано с `redirect_uri` OIDC.
- **`silentRenew` отключён** намеренно (`automaticSilentRenew: false`) — обновление токенов не выполняется в фоне.

View File

@ -0,0 +1,80 @@
# Эндпоинты, с которыми взаимодействует auth-flow-frontend
Документ описывает внешние эндпоинты, внутренние роуты и каналы межвкладочной коммуникации микрофронтенда аутентификации `auth-flow-frontend`.
## Как устроено взаимодействие
Приложение отвечает только за аутентификацию по протоколу **OIDC (Authorization Code Flow + PKCE)**. Прямых REST-запросов к бэкенд-сервисам Sarex у него **нет** — всё сетевое взаимодействие идёт с провайдером идентификации **Zitadel** через обёртку `@sarex-team/sdk-js/zitadel` (поверх `oidc-client`).
Клиент создаётся в `src/config/zitadel.ts` (`createZitadelAuth(configZitadel)`). Базовый адрес провайдера (`authority`, issuer) и `client_id` берутся из `localStorage` во время работы, а не задаются сборкой. OIDC-клиент сам находит конкретные эндпоинты issuer'а через discovery-документ `/.well-known/openid-configuration`.
## Провайдер идентификации (Zitadel) по окружениям
Фактический `authority` подставляется хостовым приложением через `localStorage` (`STORAGE.AUTHORITY`) — в самом `auth-flow-frontend` хосты не захардкожены. Ниже — известные для платформы Sarex значения (справочно):
| Окружение | `authority` (issuer) |
| --- | --- |
| `stage` | `https://idp.dev.stage.sarex.io` |
| `prod` | `https://login.sarex.io` |
## OIDC-эндпоинты провайдера
Обнаруживаются через discovery и вызываются `oidc-client` относительно `authority`. Итоговые пути определяются метаданными issuer'а.
| Эндпоинт (метаданные) | Метод | Где инициируется | Назначение |
| --- | --- | --- | --- |
| `/.well-known/openid-configuration` | GET | инициализация `UserManager` | Discovery метаданных провайдера |
| `authorization_endpoint` | GET (redirect) | `LoginPage``zitadel.authorize()` | Старт авторизации, редирект на форму входа |
| `token_endpoint` | POST | `AuthCallbackPage``zitadel.userManager.signinCallback()` | Обмен `code` → access/refresh/id токены |
| `jwks_uri` | GET | `oidc-client` | Ключи для проверки подписи токенов |
| `userinfo_endpoint` | GET | `oidc-client` (при необходимости) | Профиль пользователя |
| `end_session_endpoint` | GET (redirect) | `LogoutPage``zitadel.signout()` | Завершение сессии (логаут) |
Параметры OIDC-запросов (из `configZitadel`):
- `response_type`: `code`
- `scope`: `openid profile email offline_access urn:zitadel:iam:user:metadata`
- `redirect_uri`: `${window.location.origin}/auth/callback`
- `post_logout_redirect_uri`: `${window.location.origin}/login`
- `revokeTokensOnSignout`: `false`, `automaticSilentRenew`: `false`
## Внутренние роуты приложения
Диспетчеризация — в `src/App.tsx` по `window.location.pathname` (роуты из `src/config/urls.ts`).
| Роут | Страница | Метод | Назначение |
| --- | --- | --- | --- |
| `/auth/callback` | `AuthCallbackPage` | — | Обработка OIDC-колбэка: `signinCallback()`, установка токенов, рассылка события в `login_channel`. Дефолт для неизвестных путей |
| `/auth/error` | `AuthErrorPage` | — | Отображение ошибки аутентификации (параметры `error`, `error_description`, `state`, `message` из query) |
| `/login` | `LoginPage` (dev) | — | Кнопка входа (`zitadel.authorize()`) |
| `/logout` | `LogoutPage` (dev) | — | Кнопка выхода (`zitadel.signout()`) |
| `/` | `MainPage` (dev) | — | Dev-навигация (login/logout) |
| `/ping` | nginx | GET | Healthcheck, отдаёт `{"result": "ok"}` |
## Поток аутентификации (кратко)
1. `LoginPage` вызывает `zitadel.authorize()` → редирект на `authorization_endpoint` Zitadel.
2. Провайдер возвращает пользователя на `redirect_uri` = `/auth/callback` с `code`.
3. `AuthCallbackPage` вызывает `signinCallback()` → обмен `code` на токены через `token_endpoint`.
4. Токены сохраняются (`setTokensInfo` → `storage.setAccessToken/setRefreshToken/setIdentityToken`), снимается флаг логаута.
5. Результат рассылается остальным вкладкам, происходит закрытие overlay-окна (`window.opener`) или переход на `/` (`HOME`).
6. При ошибке — редирект на `/auth/error` с деталями.
## Межвкладочная и оконная коммуникация
| Канал | Ключ / имя | Назначение |
| --- | --- | --- |
| `BroadcastChannel` | `login_channel` | Сообщения `{ type: "login_success" }` и `{ type: "login_error", reason }` между вкладками |
| `localStorage` | `sarex_logout` (`LOGOUT_STORAGE_KEY`) | Состояние логаута между вкладками (`logoutAt`, `owner`, `reason`) |
| window events | `setTokensInfoIntoWindowEvents` (`@sarex-team/sdk-js`) | Оповещение хостового приложения об обновлении токенов (`reason: "auth_callback"`) |
| `window.opener` | — | Режим overlay: после успешного входа окно закрывается (`window.close()`) |
## Обработка ошибок
Логика — в `src/utils/authError.ts`; отображение — `AuthErrorPage`.
- Параметры ошибки читаются из query-строки колбэка: `error`, `error_description`, `state`, `message` (`parseAuthErrorFromSearch`).
- Причина ошибки: `error_description``message``error``"Unknown error"` (`getAuthErrorReason`).
- URL страницы ошибки собирается `buildAuthErrorUrl` (query из непустых параметров, иначе просто `/auth/error`).
- Если контекста ошибки нет — редирект на `/` (`HOME`).
- Пользователю доступны кнопки «Скопировать детали ошибки» (`formatAuthErrorForCopy`) и «Вернуться на форму входа» (`/login`), а также ссылка в поддержку `mailto:support@sarex.io`.

64
apps/bim/.env.example Normal file
View File

@ -0,0 +1,64 @@
# App
# APP_NAME/APP_VERSION/LOG_LEVEL читаются кодом, но в деплой-envs их обычно не задают
APP_NAME='BIM Backend v2'
APP_VERSION=local
LOG_LEVEL=info
# Адрес прослушивания HTTP API (host:port)
API_ADDRESS=0.0.0.0:5555
# Master-кластер PostgreSQL (BIM id <= LAST_MASTER_BIM)
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=bimbackendv2
POSTGRES_POOL_SIZE=10
DB_CERT_PATH=./.docker/yandex_pg.pem
# Slave-1 кластер PostgreSQL (шардирование по BIM id)
POSTGRES_ADDRESS_2=localhost
POSTGRES_PORT_2=5433
POSTGRES_USER_2=user
POSTGRES_PASSWORD_2=password
POSTGRES_DB_2=bimbackendv2
POSTGRES_POOL_SIZE_2=10
DB_CERT_PATH_2=./.docker/yandex_pg.pem
# Slave-2 кластер PostgreSQL (шардирование по BIM id)
POSTGRES_ADDRESS_3=localhost
POSTGRES_PORT_3=5434
POSTGRES_USER_3=user
POSTGRES_PASSWORD_3=password
POSTGRES_DB_3=bimbackendv2
POSTGRES_POOL_SIZE_3=10
DB_CERT_PATH_3=./.docker/yandex_pg.pem
# Границы шардирования BIM id между кластерами
LAST_MASTER_BIM=10
LAST_MASTER_BIM_V3=0
LAST_SLAVE_1_BIM=0
LAST_SLAVE_1_BIM_V3=0
# TLS до PostgreSQL (0 — локально, 1 — в облаке)
ENABLE_SSL=0
# Логировать SQL-запросы (1 — включено)
ENABLE_SQL_QUERY=1
# Внешний Django-бэкенд (проверка прав администратора)
DJANGO_HOST=https://stage.sarex.io
# Режим интеграционных тестов (в этом режиме CheckUserIsAdmin всегда true)
INTEGRATION_TESTS=0
# --- Переменные окружения, не читаемые config.go (инфраструктура/тесты/сборка) ---
# Проброс портов и локальная БД в docker-compose
# API_PORT=5555
# POSTGRES_EXTERNAL_PORT=5432
# POSTGRES_EXTERNAL_PORT_2=5433
# Значения для контейнеров тестов
# TEST_ENV=1
# TEST_BIM=1
# JWT_TEST=<jwt>
# Объявлены в .env, но config.go их не использует
# GRPC_ADDRESS=0.0.0.0
# GRPC_PORT=50051

189
apps/bim/CONFIGURATION.md Normal file
View File

@ -0,0 +1,189 @@
# Конфигурация проекта bim-backend-v2
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` (функция `NewConfig`) через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (структура `Config`).
Особенности разбора:
- **Префикса нет**`envconfig.Process("", &cfg)` вызывается с пустым префиксом, поэтому имена переменных задаются как есть (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`). Имя переменной определяется тегом `envconfig:"..."` у каждого поля.
- Типы приводятся автоматически по типу поля Go (`string`, `int`, `bool`, `uint64`). Для `bool` подходят `1`/`0`/`true`/`false`.
- Ошибка разбора приводит к `panic` при старте (в `NewConfig` `envconfig.Process` завёрнут в `sync.Once`, ошибка не возвращается, а паникует).
- Отсутствующая переменная не является ошибкой — поле получает нулевое значение соответствующего типа (пустая строка, `0`, `false`). Обязательность полей не проверяется.
В отличие от python-сервисов, приложение **загружает `.env` автоматически**: в `cmd/httpserver/main.go` вызывается `godotenv.Load(".env")` (если файла нет — печатается предупреждение и используются переменные окружения процесса).
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Файл `.env` в рабочем каталоге (`godotenv.Load`) либо переменные окружения процесса |
| Локально (контейнеры) | `.docker/docker-compose.yml`: `env_file``.docker/.env` + `.docker/.docker.env` |
| Kubernetes (Helm, репозиторий приложения) | `.helm/values-<env>.yaml`: блоки `envs` (обычные значения) и `secrets` (из k8s-секретов); шаблон `.helm/templates/api.yaml` |
| Kubernetes (IaC, этот репозиторий) | `iac/apps/bim/base/backend-deployment.yaml`: блок `env` и секреты из Vault (аннотации `vault.hashicorp.com/*`, шаблон `bim-postgresql`) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: подключение общих пайплайнов `generic/common-ci` и переменные `workflow.rules` по ветке/тегу |
Способы запуска процессов:
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `httpserver` (`make api` → `go install ./cmd/httpserver`) | `cmd/httpserver/main.go` | HTTP API. При старте применяет миграции (`appmigrations.RunOnStartup`), поднимает `net/http/pprof` на `:8081`, затем запускает API на `API_ADDRESS` |
| `migrations` (`go install ./cmd/migrations`) | `cmd/migrations/main.go` | Отдельный запуск миграций БД |
Порядок запуска в контейнере (`.docker/entrypoint.sh`): запускается `/go/bin/httpserver` (строка запуска миграций закомментирована — миграции выполняются самим приложением на старте). Финальный образ (`.docker/api.dockerfile`) — `scratch` со статически слинкованным бинарником `httpserver` и вшитым CA-сертификатом Postgres (`/root/yandex_pg.pem`).
## Переменные приложения
Ниже перечислены все переменные, читаемые кодом (`config/config.go`). Префикса нет. Дефолт — это нулевое значение типа Go, если переменная не задана.
### App
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `APP_NAME` | string | `""` | Имя приложения (в коде помечено TODO — в deploy-envs не задаётся) |
| `APP_VERSION` | string | `""` | Версия приложения (TODO — в deploy-envs не задаётся) |
| `LOG_LEVEL` | string | `""` | Уровень логирования, передаётся в `logging.NewLogger` (TODO — в deploy-envs не задаётся) |
| `API_ADDRESS` | string | `""` | Адрес прослушивания HTTP API в формате `host:port` (напр. `0.0.0.0:8080`) |
| `TEST_ENV` | string | `""` | Служебное поле для тестов |
### PostgreSQL
Сервис работает с **тремя** кластерами PostgreSQL (master, slave-1, slave-2). Выбор кластера для конкретного BIM выполняется по его id в `internal/app/http/httpserver.go` (сравнение с `LAST_MASTER_BIM*`/`LAST_SLAVE_1_BIM*`). Строка подключения собирается в `Config.GetPostgresConnectionURL` (`postgres://user:password@addr:port/db`, при `ENABLE_SSL=false` добавляется `?sslmode=disable`).
Master-кластер:
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `POSTGRES_ADDRESS` | string | Хост PostgreSQL (master) |
| `POSTGRES_PORT` | string | Порт PostgreSQL (master) |
| `POSTGRES_USER` | string | Пользователь БД |
| `POSTGRES_PASSWORD` | string | Пароль пользователя БД |
| `POSTGRES_DB` | string | Имя базы данных |
| `POSTGRES_POOL_SIZE` | int | Размер пула соединений |
| `DB_CERT_PATH` | string | Путь к CA-сертификату PostgreSQL (используется при `ENABLE_SSL=1`) |
Slave-1 кластер — те же поля с суффиксом `_2`:
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `POSTGRES_ADDRESS_2` | string | Хост PostgreSQL (slave-1) |
| `POSTGRES_PORT_2` | string | Порт |
| `POSTGRES_USER_2` | string | Пользователь |
| `POSTGRES_PASSWORD_2` | string | Пароль |
| `POSTGRES_DB_2` | string | База данных |
| `POSTGRES_POOL_SIZE_2` | int | Размер пула |
| `DB_CERT_PATH_2` | string | Путь к CA-сертификату |
Slave-2 кластер — те же поля с суффиксом `_3`:
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `POSTGRES_ADDRESS_3` | string | Хост PostgreSQL (slave-2) |
| `POSTGRES_PORT_3` | string | Порт |
| `POSTGRES_USER_3` | string | Пользователь |
| `POSTGRES_PASSWORD_3` | string | Пароль |
| `POSTGRES_DB_3` | string | База данных |
| `POSTGRES_POOL_SIZE_3` | int | Размер пула |
| `DB_CERT_PATH_3` | string | Путь к CA-сертификату |
### Шардирование BIM по кластерам
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `LAST_MASTER_BIM` | uint64 | Верхняя граница id BIM для master-кластера (API v1) |
| `LAST_MASTER_BIM_V3` | uint64 | Верхняя граница id BIM для master-кластера (API v2 / BIM v3) |
| `LAST_SLAVE_1_BIM` | uint64 | Верхняя граница id BIM для slave-1 (API v1) |
| `LAST_SLAVE_1_BIM_V3` | uint64 | Верхняя граница id BIM для slave-1 (API v2 / BIM v3) |
### Прочее
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_SSL` | bool | `false` | Подключение к PostgreSQL по TLS. При `false` в DSN добавляется `sslmode=disable` |
| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы |
| `DJANGO_HOST` | string | `""` | Базовый URL Django-бэкенда для проверки прав администратора (см. `ENDPOINTS.md`) |
| `INTEGRATION_TESTS` | bool | `false` | Режим интеграционных тестов. При `true` `CheckUserIsAdmin` всегда возвращает `true` |
## Переменные, не читаемые приложением (инфраструктура/сборка/тесты)
Присутствуют в `.docker/.env`, `.docker/.docker.env`, `docker-compose.yml` или Helm, но `config/config.go` их не разбирает.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `API_PORT` | `.docker/.env` | Порт API для локального compose |
| `POSTGRES_EXTERNAL_PORT`, `POSTGRES_EXTERNAL_PORT_2` | `docker-compose.yml` | Проброс портов контейнеров Postgres |
| `POSTGRES_ADDRESS2` | `.docker/.docker.env` | Опечатка/легаси (нет суффикса `_`), кодом не читается |
| `GRPC_ADDRESS`, `GRPC_PORT` | `.docker/.env` | Объявлены, но кодом не используются |
| `TEST_BIM`, `JWT_TEST` | `.docker/.env`, `.docker/.docker.env` | Данные для интеграционных тестов |
| `POSTGRES_*_4`, `DB_CERT_PATH_4` | `.helm/values-*.yaml`, IaC `backend-deployment.yaml` | Четвёртый кластер задаётся в деплое, но `config.go` доходит только до суффикса `_3` |
| `LAST_SLAVE_2_BIM(_V3)`, `LAST_SLAVE_3_BIM(_V3)`, `LAST_SLAVE_4_BIM(_V3)` | `.helm/values-*.yaml` | Заданы в values, но кодом не читаются (актуальны только `LAST_MASTER_*` и `LAST_SLAVE_1_*`) |
| `CI_COMMIT_SHORT_SHA` | `.gitlab-ci.yml` (build-arg) | Тег/версия сборки образа |
## Переменные из Helm-чарта приложения (`.helm/values-<env>.yaml`)
Обычные значения задаются в блоке `envs` для каждого окружения (`stage`/`preprod`/`production`) и содержат те же переменные `POSTGRES_*`, `API_ADDRESS`, `DJANGO_HOST`, `ENABLE_SSL`, `ENABLE_SQL_QUERY`, `LAST_*`, что описаны выше (различаются адресами БД, размерами пула, границами шардирования и доменом Django).
Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) |
| --- | --- | --- |
| `POSTGRES_USER` | `bim-v2-database-secret` | `username` |
| `POSTGRES_PASSWORD` | `bim-v2-database-secret` | `password` |
| `POSTGRES_USER_2` | `bim-v2-database-secret-2` | `username` |
| `POSTGRES_PASSWORD_2` | `bim-v2-database-secret-2` | `password` |
| `POSTGRES_USER_3` | `bim-v2-database-secret-3` | `username` |
| `POSTGRES_PASSWORD_3` | `bim-v2-database-secret-3` | `password` |
| `POSTGRES_USER_4` | `bim-v2-database-secret-3` | `username` |
| `POSTGRES_PASSWORD_4` | `bim-v2-database-secret-3` | `password` |
Ключевые не-env значения чарта: `api.name`, `api.image`, `api.version`, `api.port` (`8080`), `api.replicas`, `api.service_name`/`api.service_port` (`80`), `api.service_account`, `api.requests.memory`/`cpu`, `api.api_host`, `api.api_host_prefix` (`/bimv2/api/`), `api.api_path` (`/api/`), `api.internal_path` (`/internal/`), `api.permitted_ns` (namespace'ы, которым разрешён доступ к `/internal/*`), `imagePullSecrets`.
Сетевой слой (`.helm/templates/mesh-config.yaml`, Istio):
- `VirtualService` (при `api.virtual_service.enabled`) маршрутизирует `api_host_prefix``api_path`, задаёт CORS (`allowOrigins` — `*.sarex.io` и `localhost`, `allowHeaders` включают `Authorization`, `Content-Type`, `Identity`).
- `AuthorizationPolicy` `routes-v2`: доступ к `/api/*` разрешён только от istio-ingressgateway при наличии claim `token_type=access`; доступ к `/internal/*` — только из namespace'ов `api.permitted_ns`.
## Переменные из IaC-репозитория (`iac/apps/bim/`)
Деплой в этом репозитории (kustomize) устроен иначе, чем чарт приложения:
- `base/backend-deployment.yaml` — Deployment `backend` в namespace `bim`, образ `bim-api`, `containerPort: 8000`, `API_ADDRESS=0.0.0.0:8000`, health-проба `GET /ping`.
- Секреты PostgreSQL инъектируются **из Vault** (аннотации `vault.hashicorp.com/*`, роль `bim`, путь `secrets/data/postgresql/apps/bim`) в файл `/vault/secrets/bim-postgresql`, который экспортируется в окружение перед запуском (`set -a; . /vault/secrets/bim-postgresql; set +a; exec ./httpserver`). Шаблон Vault задаёт `POSTGRES_ADDRESS[_2../_4]`, `POSTGRES_PORT*`, `POSTGRES_DB*` (все указывают на `postgresql.bim.svc.cluster.local:5432`, БД `bim_db`) и `POSTGRES_USER*`/`POSTGRES_PASSWORD*` из Vault.
- Прочие env заданы прямо в манифесте: `LAST_MASTER_BIM`, `LAST_MASTER_BIM_V3`, `LAST_SLAVE_1_BIM`, `POSTGRES_POOL_SIZE`, `DB_CERT_PATH_2/3/4`, `DJANGO_HOST` (`http://backend.django.svc.cluster.local:8000`), `ENABLE_SQL_QUERY=0`, `ENABLE_SSL=0`.
- Оверлеи `brusnika-prod`, `brusnika-stage`, `yc-k8s-test` патчат base (реплики, образ, postgresql).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` (`universal-pipeline-<env>.yaml`, `common-security-scan.yaml`, `common-build.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace |
| --- | --- | --- |
| ветка `master` | `preprod` | `bim-api-preprod` |
| ветка `stage` | `stage` | `bim-api-stage` |
| тег (`CI_COMMIT_TAG`) | `prod` | `bim-api-prod` |
Общие переменные job'ов: `RELEASE_NAME=bim-backend-v2`, `CHART_NAME=bim-backend-v2`, `CHART_VERSION=0.0.1-<stand>`, `IMAGE_PATH=api.image`, `DOCKERFILE_PATH=.docker/api.dockerfile`, `HELM_SET_ARGS=--set api.image=${IMAGE_NAME}`, `BUILD_ARGS=--build-arg CI_COMMIT_SHORT_SHA=...`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY=true`, `ENABLE_LINTER=false`. Стадии: `linter → test → unittest → prebuild-secscan → build → state-update → deploy`.
## Замечания и потенциальные проблемы
- Переменные разбираются **без префикса** (`envconfig.Process("", ...)`) — имена совпадают с тегами `envconfig` полей структуры `Config`.
- Обязательность полей не валидируется: отсутствующая переменная молча получает нулевое значение. Например пустой `API_ADDRESS` приведёт к попытке слушать на `":0"`.
- В коде объявлены только три кластера (`POSTGRES_*`, `_2`, `_3`), тогда как в деплое присутствует и четвёртый (`_4`). Переменные `_4` и дополнительные `LAST_SLAVE_2/3/4_*` в окружении задаются, но приложением не используются.
- Приложение загружает `.env` автоматически (`godotenv.Load(".env")`); при отсутствии файла ошибки нет — берутся переменные процесса.
- В `NewConfig` ошибка `envconfig.Process` не возвращается, а вызывает `panic` (обёрнута в `sync.Once`).
## Минимальный набор для локального запуска
Postgres поднимается через `docker-compose` (`.docker/docker-compose.yml`), приложение — через `make api` и запуск `httpserver`. Минимально необходимо задать (готовые примеры — в `.env.example`):
- `API_ADDRESS`
- Master-кластер: `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE`
- Slave-кластеры `_2` и `_3` (те же поля) — сервис создаёт соединения ко всем трём при старте
- `ENABLE_SSL=0`, `DB_CERT_PATH*` (при `ENABLE_SSL=1`)
- `DJANGO_HOST`
- `LAST_MASTER_BIM`, `LAST_MASTER_BIM_V3`, `LAST_SLAVE_1_BIM`, `LAST_SLAVE_1_BIM_V3`
- `INTEGRATION_TESTS=0`, `ENABLE_SQL_QUERY` по необходимости

55
apps/bim/ENDPOINTS.md Normal file
View File

@ -0,0 +1,55 @@
# Эндпоинты, с которыми взаимодействует bim-backend-v2
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается сам сервис `bim-backend-v2` (исходящие вызовы). Эндпоинты, которые сервис **предоставляет**, описаны в `openapi.yaml`.
## Как устроено взаимодействие
Исходящие вызовы выполняются HTTP-клиентом на базе [`go-resty/resty`](https://github.com/go-resty/resty) в пакете `pkg/django_client` (`DjangoClient`). Клиент создаётся в `NewRestClient`:
- базовый хост — `SetHostURL(cfg.DjangoHost)` (переменная `DJANGO_HOST`);
- таймаут запроса — `SetTimeout(10 * time.Second)`;
- число повторов — `SetRetryCount(5)`.
Аутентификация проксируется: JWT пользователя извлекается из контекста запроса (`auth.JWTFromContext`), при необходимости отбрасывается схема `Bearer `, и токен передаётся во внешний сервис заголовком `Authorization: Bearer <jwt>` (`SetAuthToken` + `SetAuthScheme("Bearer")`). Заголовок `Content-Type: application/json`.
В режиме интеграционных тестов (`INTEGRATION_TESTS=1`) внешний вызов не выполняется — `CheckUserIsAdmin` возвращает `true`.
## Базовые хосты по сервисам и окружениям
Значение берётся из переменной `DJANGO_HOST` (см. `CONFIGURATION.md`). Итоговый URL = `<DJANGO_HOST>` + `path` эндпоинта.
| Сервис | Назначение | Значение `DJANGO_HOST` |
| --- | --- | --- |
| `django` (Sarex backend) | Проверка прав пользователя (админ/не админ) | локально/`stage`: `https://stage.sarex.io`; `preprod`: `https://lk.preprod.sarex.io`; `prod`: `https://lk.sarex.io`; контур (IaC): `http://backend.django.svc.cluster.local:8000` |
## Эндпоинты по сервисам
### `django` — Sarex backend
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `api/client/settings/` | Получить настройки текущего пользователя. Используется поле `is_admin` (проверка прав администратора в `CheckUserIsAdmin`) |
Детали вызова `GET api/client/settings/`:
| Параметр | Значение |
| --- | --- |
| Заголовки | `Authorization: Bearer <jwt пользователя>`, `Content-Type: application/json` |
| Тело запроса | нет |
| Ожидаемый ответ | `{ "is_admin": bool }` (структура `userSettingsFromDjango`) |
| Успешные статусы | `200`, `201` |
| Поведение при ошибке | Любая ошибка транспорта, `resp == nil` или статус вне `200/201` логируется, метод трактует пользователя как **не администратора** (`false`) |
## Где используется
Проверка `CheckUserIsAdmin` вызывается в обработчиках, изменяющих модели статусов (требуют прав администратора). При отсутствии прав такие эндпоинты возвращают `403 No admin rights`:
- `POST /api/v1/bims/{bim_id}/status_model` — создание модели статусов BIM;
- `DELETE /api/v1/bims/{bim_id}/delete_status_model` — удаление модели статусов BIM;
- `POST /api/v1/companies/{company_id}/status_model` — создание модели статусов компании;
- `DELETE /api/v1/companies/{company_id}/status_model` — удаление модели статусов компании.
## Замечания
- Единственная внешняя HTTP-зависимость сервиса — Django-бэкенд (`DJANGO_HOST`). Прочие интеграции (PostgreSQL, pprof) не являются HTTP-вызовами к внешним REST-сервисам.
- Подпись JWT самим сервисом не проверяется — проброшенный токен просто пересылается в Django, который и выполняет авторизацию (см. также раздел «Аутентификация» в `openapi.yaml`).

1158
apps/bim/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

80
apps/cde/.env.example Normal file
View File

@ -0,0 +1,80 @@
# =============================================================================
# cde-orchestration-demo (Оркестратор) — пример переменных окружения
#
# Разбор переменных: github.com/sethvargo/go-envconfig.
# Файл .env подгружается через godotenv (флаг -env-file, по умолчанию `.env`).
# Глобального префикса у переменных НЕТ (в отличие от других сервисов Sarex).
# Вложенные секции задаются префиксом в env-тегах (напр. DATABASE_URL).
#
# Значения ниже — примеры и значения по умолчанию из кода. Секреты (ключи,
# пароли, токены) оставлены пустыми — заполните собственными.
# Один и тот же .env используется всеми бинарниками (см. docker-compose.yml),
# но каждый бинарник читает только нужное ему подмножество (см. CONFIGURATION.md).
# =============================================================================
# --- Общие ---
ENVIRONMENT=production
LOG_LEVEL=info # cmd/http: default=info; воркеры: default=debug
IS_CONTOUR=false
# --- HTTP-сервер (cmd/http) ---
ADDRESS=:8080
PROCESS_CACHE_TTL=60
PROCESS_CACHE_CLEAR_INTERVAL=60
PUBLIC_KEY= # PEM RSA public key для проверки JWT (обязателен для http)
OPERATE_URL= # Базовый URL Camunda Operate/REST
SAREX_BACKEND_BASE_URL=https://stage.sarex.io
# --- Camunda ---
CAMUNDA_KEYCLOAK_URL=
CAMUNDA_CLIENT_ID=operate # только cmd/http
CAMUNDA_CLIENT_SECRET=identity-secret-for-components # только cmd/http
CAMUNDA_PROCESS_DEFINITION_ID=actionsOnApproval # только cmd/http
# --- Zeebe ---
ZEEBE_GATEWAY=
ZEEBE_CLIENT_ID=zeebe
ZEEBE_CLIENT_SECRET=identity-secret-for-components
ZEEBE_WORKER_JOB_TYPE=markDocuments # у каждого воркера своё значение по умолчанию (см. CONFIGURATION.md)
# --- Database (PostgreSQL) ---
DATABASE_URL=
DATABASE_POOL_SIZE=10
# --- S3 ---
S3_ENDPOINT_URL=https://storage.yandexcloud.net
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_PARTITION_ID=yc
S3_SIGNING_REGION=ru-central1
# --- Auth (без префикса) ---
AUTH_HOST=
USERNAME=
PASSWORD=
# --- Flows (сервис рабочих процессов) ---
FLOWS_URL=
FLOWS_INTERNAL_URL=
# --- Workspaces ---
WORKSPACES_URL=
# --- Workflows (используется воркером split_pdf) ---
WORKFLOWS_HOST=
WORKFLOWS_IMAGE_TAG=latest
# --- System log ---
SYSTEM_LOG_URL=
# --- Telegram (алертинг воркеров) ---
TELEGRAM_TOKEN=
TELEGRAM_ALERT_GROUP_ID=
TELEGRAM_DEBUG=false
# --- AMQP / RabbitMQ (markings v2, copy v2) ---
AMQP_HOST=
AMQP_PORT=
AMQP_USER=
AMQP_PASSWORD=
AMQP_PATH_API=

215
apps/cde/CONFIGURATION.md Normal file
View File

@ -0,0 +1,215 @@
# Конфигурация проекта cde-orchestration-demo (Оркестратор)
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig): в каждом бинарнике вызывается `envconfig.Process(ctx, config)` со своей структурой `Config` (см. `internal/app/http/config.go` и `internal/app/worker/*/config.go`).
Особенности разбора:
- **Глобального префикса нет** — в отличие от других сервисов Sarex, переменные не имеют общего префикса (напр. просто `LOG_LEVEL`, `DATABASE_URL`).
- Вложенные секции задаются тегом `env:", prefix=XXX_"` на поле-структуре. Например поле `Database DatabaseConfig` с `prefix=DATABASE_` и полем `Url` с тегом `env:"URL"` даёт переменную `DATABASE_URL`.
- Структура `Auth` **не имеет** тега `prefix`, поэтому её поля читаются без префикса: `AUTH_HOST`, `USERNAME`, `PASSWORD`.
- Значения по умолчанию задаются в теге через `default=...`. Отсутствие поля без дефолта не приводит к ошибке `envconfig` (пустое значение), но может привести к падению при инициализации зависимого клиента (напр. пустой `PUBLIC_KEY` вызовет панику при старте http).
Файл `.env` подгружается через [`github.com/lpernett/godotenv`](https://github.com/lpernett/godotenv): в `main` вызывается `godotenv.Load(*envFileFlag)`, путь задаётся флагом `-env-file` (по умолчанию `.env`). Если файла нет — загрузка пропускается, переменные берутся из окружения процесса.
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Флаг `-env-file``.env` (godotenv) и/или переменные окружения процесса |
| Локально (docker-compose) | `docker-compose.yml`: у каждого сервиса `env_file: .env` |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секрета `cde-secret`) чарта `universal-chart` |
| Kubernetes (kustomize, этот репозиторий) | Vault Agent инжектит секрет `secrets/data/vault/apps/cde` в файл `/vault/secrets/cde-env`, который экспортируется в окружение перед запуском бинарника (`source /vault/secrets/cde-env`) |
## Бинарники (точки входа)
| Бинарник | Точка входа | Config | Назначение |
| --- | --- | --- | --- |
| `http` | `cmd/http/main.go` | `internal/app/http` | HTTP API оркестрации (процессы, подпись, загрузка BPMN) |
| `copy` | `cmd/worker/copy/main.go` | `.../worker/copy` | Воркер копирования документов (`copyDocuments`) |
| `copyv2` | `cmd/worker/copyv2/main.go` | `.../worker/copyv2` | Копирование документов v2 (`copyDocumentsv2`) |
| `create_versions` | `cmd/worker/create_versions/main.go` | `.../worker/create_versions` | Создание версий (`createVersions`) |
| `create_versionsv2` | `cmd/worker/create_versionsv2/main.go` | `.../worker/create_versionsv2` | Создание версий v2 (`createVersionsv2`) |
| `flows_callback` | `cmd/worker/flows_callback/main.go` | `.../worker/flows_callback` | Обратный вызов в сервис flows (`flowsCallback`) |
| `markings` | `cmd/worker/markings/main.go` | `.../worker/markings` | Маркировка документов (`markDocuments`) |
| `markingsv2` | `cmd/worker/markingsv2/main.go` | `.../worker/markingsv2` | Маркировка v2 (`markDocumentsv2`) |
| `sign` | `cmd/worker/sign/main.go` | `.../worker/sign` | Подпись документов (`signDocuments`) |
| `signv2` | `cmd/worker/signv2/main.go` | `.../worker/signv2` | Подпись v2 (`signDocumentsv2`) |
| `split_pdf` | `cmd/worker/split_pdf/main.go` | `.../worker/split_pdf` | Разбиение/обработка PDF (`splitPDF`) |
| `update_bundles` | `cmd/worker/update_bundles/main.go` | `.../worker/update_bundles` | Обновление бандлов (`updateBundles`) |
Воркеры — это Zeebe job-workers: они подключаются к Zeebe-gateway и обрабатывают Service Task соответствующего типа (`ZEEBE_WORKER_JOB_TYPE`). HTTP-сервер, помимо приёма запросов, обращается к Camunda Operate/Zeebe (см. `ENDPOINTS.md`).
## Переменные приложения
Дефолт `—` означает, что значения по умолчанию нет.
### Общие (для всех бинарников)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENVIRONMENT` | string | `production` (у воркеров) | Окружение развёртывания. У `http` не читается |
| `LOG_LEVEL` | string | `info` (http) / `debug` (воркеры) | Уровень логирования |
| `IS_CONTOUR` | bool | `false` | Режим изолированного контура (влияет на инициализацию S3) |
### HTTP-сервер (`cmd/http`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ADDRESS` | string | `:8080` | Адрес прослушивания Fiber |
| `PROCESS_CACHE_TTL` | int (сек) | `60` | TTL кеша процессов |
| `PROCESS_CACHE_CLEAR_INTERVAL` | int (сек) | `60` | Интервал очистки кеша процессов |
| `PUBLIC_KEY` | string (PEM) | — | RSA public key для проверки JWT. Обязателен: при пустом/некорректном значении сервис падает при старте |
| `OPERATE_URL` | string | — | Базовый URL Camunda Operate/REST |
| `SAREX_BACKEND_BASE_URL` | string | — | Базовый URL sarex-backend (проверка MRPA при подписи) |
### Camunda (`CAMUNDA_*`)
| Переменная | Тип | Значение по умолчанию | Назначение | Где используется |
| --- | --- | --- | --- | --- |
| `CAMUNDA_KEYCLOAK_URL` | string | — | URL Keycloak для OAuth (Zeebe/Operate) | http + воркеры |
| `CAMUNDA_CLIENT_ID` | string | `operate` | Client ID для Operate | только http |
| `CAMUNDA_CLIENT_SECRET` | string | `identity-secret-for-components` | Client secret для Operate | только http |
| `CAMUNDA_PROCESS_DEFINITION_ID` | string | `actionsOnApproval` | ID определения процесса | только http |
### Zeebe (`ZEEBE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ZEEBE_GATEWAY` | string | — | Адрес Zeebe gateway |
| `ZEEBE_CLIENT_ID` | string | `zeebe` | Client ID |
| `ZEEBE_CLIENT_SECRET` | string | `identity-secret-for-components` | Client secret |
| `ZEEBE_WORKER_JOB_TYPE` | string | зависит от воркера | Тип Service Task, который слушает воркер |
Значения `ZEEBE_WORKER_JOB_TYPE` по умолчанию: `markDocuments` (http/markings), `markDocumentsv2` (markingsv2), `copyDocuments` (copy), `copyDocumentsv2` (copyv2), `createVersions` (create_versions), `createVersionsv2` (create_versionsv2), `flowsCallback` (flows_callback), `signDocuments` (sign), `signDocumentsv2` (signv2), `splitPDF` (split_pdf), `updateBundles` (update_bundles).
### Database (`DATABASE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DATABASE_URL` | string | — | DSN подключения к PostgreSQL (pgx) |
| `DATABASE_POOL_SIZE` | int32 | `10` | Размер пула соединений |
### S3 (`S3_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_ENDPOINT_URL` | string | `https://storage.yandexcloud.net` | Эндпоинт S3 |
| `S3_ACCESS_KEY_ID` | string | — | Access key |
| `S3_SECRET_ACCESS_KEY` | string | — | Secret key |
| `S3_PARTITION_ID` | string | `yc` | Partition ID (aws-sdk-go-v2) |
| `S3_SIGNING_REGION` | string | `ru-central1` | Регион для подписи запросов |
### Auth (без префикса)
Поле-структура `Auth` не имеет префикса, поэтому переменные читаются напрямую.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AUTH_HOST` | string | — | Хост сервиса аутентификации (получение токенов пользователя/админа) |
| `USERNAME` | string | — | Логин админ-учётки |
| `PASSWORD` | string | — | Пароль админ-учётки |
### Flows (`FLOWS_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `FLOWS_URL` | string | — | Базовый URL сервиса flows |
| `FLOWS_INTERNAL_URL` | string | — | Внутренний URL flows (обновление документов review). Есть только в конфигах `copy`/`copyv2` |
### Workspaces (`WORKSPACES_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKSPACES_URL` | string | — | URL сервиса рабочих областей (используется воркерами copy/copyv2) |
### Workflows (`WORKFLOWS_*`)
Используется воркером `split_pdf`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOWS_HOST` | string | — | Хост сервиса workflows |
| `WORKFLOWS_IMAGE_TAG` | string | `latest` | Тег docker-образа задач обработки PDF |
> Container registry (`cr.yandex/crp3ccidau046kdj8g9q`) и флаг `UploadResultsToS3=true` заданы в коде воркера `split_pdf` (`worker.go`), а не через окружение.
### System log (`SYSTEM_LOG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SYSTEM_LOG_URL` | string | — | URL сервиса системных логов (copy, copyv2, create_versions, create_versionsv2) |
### Telegram (`TELEGRAM_*`)
Клиент алертинга для воркеров.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TELEGRAM_TOKEN` | string | — | Токен бота |
| `TELEGRAM_ALERT_GROUP_ID` | int64 | — | ID группы для алертов |
| `TELEGRAM_DEBUG` | bool | `false` | Debug-режим бота |
### AMQP / RabbitMQ (`AMQP_*`)
Используется воркерами `markingsv2` и `copyv2` (маркировка бандлов через RabbitMQ).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AMQP_HOST` | string | — | Хост RabbitMQ |
| `AMQP_PORT` | string | — | Порт RabbitMQ |
| `AMQP_USER` | string | — | Пользователь |
| `AMQP_PASSWORD` | string | — | Пароль |
| `AMQP_PATH_API` | string | — | Vhost / путь API в URL подключения |
## Матрица «переменная → бинарник»
| Секция | http | copy | copyv2 | create_versions | create_versionsv2 | flows_callback | markings | markingsv2 | sign | signv2 | split_pdf | update_bundles |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Общие | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `ADDRESS`/`PROCESS_CACHE_*` | ✓ | | | | | | | | | | | |
| `PUBLIC_KEY`,`OPERATE_URL`,`SAREX_BACKEND_BASE_URL`,`CAMUNDA_CLIENT_*`,`CAMUNDA_PROCESS_DEFINITION_ID` | ✓ | | | | | | | | | | | |
| `CAMUNDA_KEYCLOAK_URL`,`ZEEBE_*` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `DATABASE_*` | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| `S3_*` | ✓ | ✓ | ✓ | ✓ | ✓ | | | ✓ | ✓ | ✓ | | |
| `AUTH_*`/`USERNAME`/`PASSWORD` | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `FLOWS_URL` | | ✓ | ✓ | | | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ |
| `FLOWS_INTERNAL_URL` | | ✓ | ✓ | | | | | | | | | |
| `WORKSPACES_URL` | | ✓ | ✓ | | | | | | | | | |
| `WORKFLOWS_*` | | | | | | | | | | | ✓ | |
| `SYSTEM_LOG_URL` | | ✓ | ✓ | ✓ | ✓ | | | | | | | |
| `AMQP_*` | | | ✓ | | | | | ✓ | | | | |
| `TELEGRAM_*` | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
> Матрица построена по структурам `Config` соответствующих бинарников. Наличие поля в структуре не всегда означает, что клиент инициализируется — см. замечания ниже.
## Переменные в Helm-чарте (`.helm/values.yaml`)
Обычные значения (блок `envs`):
| Переменная | Значения по окружениям |
| --- | --- |
| `SAREX_BACKEND_BASE_URL` | stage: `https://stage.sarex.io`, preprod: `https://preprod.sarex.io`, production: `https://lk.sarex.io` |
Значения из секрета (блок `secretEnvs`, общий для всех сервисов через якорь `*cde_secret_envs`), берутся из k8s-секрета `cde-secret` одноимёнными ключами: `ENVIRONMENT`, `LOG_LEVEL`, `ZEEBE_GATEWAY`, `DATABASE_URL`, `S3_ENDPOINT_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `PUBLIC_KEY`, `PDM_URL`, `FLOWS_URL`, `FLOWS_INTERNAL_URL`, `USERNAME`, `PASSWORD`, `CAMUNDA_PROCESS_DEFINITION_ID`, `OPERATE_URL`, `CAMUNDA_KEYCLOAK_URL`, `CAMUNDA_CLIENT_ID`, `CAMUNDA_CLIENT_SECRET`, `WORKFLOWS_HOST`, `WORKSPACES_URL`, `AUTH_HOST`, `TELEGRAM_ALERT_GROUP_ID`, `TELEGRAM_TOKEN`, `IS_CONTOUR`, `AMQP_HOST`, `AMQP_PORT`, `AMQP_USER`, `AMQP_PASSWORD`, `AMQP_PATH_API`, `SYSTEM_LOG_URL`.
## Развёртывание через kustomize (этот репозиторий)
В `iac/apps/cde` секреты доставляются не через `secretEnvs` чарта, а через **Vault Agent Injector**: аннотации подов монтируют секрет `secrets/data/vault/apps/cde` в файл `/vault/secrets/cde-env`, который экспортируется перед запуском (`source /vault/secrets/cde-env`, затем `exec /http` или `/worker`). Дополнительно контейнерам задаётся `S3_IS_CONTOUR=true` (примечание: в коде используется переменная `IS_CONTOUR`).
Оверлеи: `base` (общие манифесты), `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`.
## Замечания и потенциальные проблемы
- **Нет глобального префикса.** Имена переменных короткие (`USERNAME`, `PASSWORD`, `AUTH_HOST`) — легко пересечься с системными; следите за окружением процесса.
- **`PUBLIC_KEY` обязателен для `http`** — при пустом/некорректном PEM сервис паникует на старте (`server.go`).
- **`PDM_URL`** присутствует в `secretEnvs` Helm, но соответствующий клиент (`internal/adapters/http/pdm`) в текущей сборке нигде не инициализируется — переменная фактически не используется кодом.
- **`S3_IS_CONTOUR`** задаётся в kustomize-манифестах, тогда как код читает `IS_CONTOUR` (без префикса `S3_`). Проверьте, что для влияния на поведение выставлен именно `IS_CONTOUR`.
- **`FLOWS_INTERNAL_URL`** объявлен только в конфигах `copy`/`copyv2`; в остальных воркерах поля нет, хотя ключ есть в общем секрете.
- **Значения по умолчанию для секретов Camunda/Zeebe** (`identity-secret-for-components`) подходят для локального стенда, но должны переопределяться в prod.
- Приложение читает `.env` только если файл существует; иначе используются переменные окружения. `make`-целей для генерации `.env` в репозитории нет — используйте этот `.env.example` как шаблон.

130
apps/cde/ENDPOINTS.md Normal file
View File

@ -0,0 +1,130 @@
# Эндпоинты внешних сервисов, с которыми взаимодействует cde-orchestration-demo
Документ описывает все HTTP/AMQP/gRPC-эндпоинты внешних сервисов, к которым обращается оркестратор (сервер `cmd/http` и воркеры). Это исходящие вызовы; описание API, который оркестратор **предоставляет**, — в `openapi.yaml`.
## Как устроено взаимодействие
Клиенты внешних сервисов лежат в `internal/adapters/http/*` и `internal/adapters/*`. Базовые URL берутся из переменных окружения (см. `CONFIGURATION.md`). Для HTTP используются два клиента: Fiber `client` (camunda, flows, pdm, workspaces, system_log) и `go-resty` (workflows, sarexbackend). Аутентификация — по-разному в зависимости от сервиса (OAuth client_credentials, Bearer-токен пользователя/админа, Basic).
## Базовые адреса по сервисам
| Сервис | Переменная базового адреса | Клиент | Назначение |
| --- | --- | --- | --- |
| Camunda Operate / REST | `OPERATE_URL` | Fiber | Управление инстансами процессов, переменными, сообщениями |
| Camunda Keycloak | `CAMUNDA_KEYCLOAK_URL` | Fiber | OAuth-токен для Operate |
| Zeebe Gateway | `ZEEBE_GATEWAY` | gRPC (SDK) | Деплой BPMN, обработка job'ов воркерами |
| Auth (токены) | `AUTH_HOST` | Fiber/resty | Токены пользователя/админа для flows и pdm |
| Flows | `FLOWS_URL`, `FLOWS_INTERNAL_URL` | Fiber | Ревью, действия пользователя, обновление бандлов/документов |
| PDM | `PDM_URL` | Fiber | Маркировка бандлов *(клиент не подключён — см. примечание)* |
| Workflows | `WORKFLOWS_HOST` | resty | Создание workflow обработки PDF |
| Workspaces | `WORKSPACES_URL` | Fiber | Создание рабочих областей |
| System log | `SYSTEM_LOG_URL` | Fiber | Отправка системных логов |
| Sarex backend | `SAREX_BACKEND_BASE_URL` | resty | Получение MRPA по id |
| Telegram Bot API | `TELEGRAM_TOKEN` | tgbotapi | Алертинг воркеров |
| RabbitMQ (AMQP) | `AMQP_*` | amqp091 | Маркировка бандлов (RPC), вычисление хеш-сумм |
## Эндпоинты по сервисам
### Camunda Operate / REST (`OPERATE_URL`, `CAMUNDA_KEYCLOAK_URL`)
`internal/adapters/http/camunda/client.go`. Все запросы (кроме получения токена) идут с заголовком `Authorization: Bearer <access_token>`.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{CAMUNDA_KEYCLOAK_URL}/auth/realms/camunda-platform/protocol/openid-connect/token` | OAuth-токен (`grant_type=client_credentials`) |
| POST | `{OPERATE_URL}/v2/process-instances` | Создать инстанс процесса |
| GET | `{OPERATE_URL}/api/process-instances/{key}` | Получить инстанс процесса |
| POST | `{OPERATE_URL}/v1/variables/search` | Поиск переменных процесса по имени/значению |
| GET | `{OPERATE_URL}/api/process-instances/{key}/variables/{varId}` | Значение конкретной переменной |
| POST | `{OPERATE_URL}/api/process-instances/{key}/variables` | Список переменных инстанса (`scopeId`) |
| POST | `{OPERATE_URL}/v2/messages/publication` | Публикация сообщения процессу (напр. `signRequest`) |
### Zeebe Gateway (`ZEEBE_GATEWAY`)
`internal/adapters/zeebe`. gRPC через официальный SDK `camunda/zeebe/clients/go/v8`. Используется для деплоя определений процессов (`NewProcessDefinition`) и для job-воркеров, которые слушают Service Task типа `ZEEBE_WORKER_JOB_TYPE` и по завершении/ошибке возвращают результат в инстанс процесса.
### Auth — токены (`AUTH_HOST`)
Используется адаптерами flows и pdm для получения Bearer-токенов.
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `{AUTH_HOST}/token/user/{userID}/` | Токен от имени пользователя |
| POST | `{AUTH_HOST}/token/` | Токен админа (`username`/`password`) |
### Flows (`FLOWS_URL`, `FLOWS_INTERNAL_URL`)
`internal/adapters/http/flows/adapter.go`. Запросы (кроме `update-documents`) идут с `Authorization: Bearer <token>`; часть операций — с ретраями (экспоненциальный backoff).
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{FLOWS_URL}/user-actions/` | Добавить действие в историю |
| PATCH | `{FLOWS_URL}/reviews/{reviewID}/approve/` | Утвердить review (со статусом/комментарием) |
| PATCH | `{FLOWS_URL}/reviews/{reviewID}/update-bundles/` | Обновить бандлы review |
| PATCH | `{FLOWS_INTERNAL_URL}/reviews/{reviewID}/update-documents/` | Обновить документы review (внутренний URL, без авторизации) |
| GET | `{FLOWS_URL}/reviews/{reviewID}/documents/` | История бандлов документов review |
### PDM (`PDM_URL`, `AUTH_HOST`) — не подключён
`internal/adapters/http/pdm/client.go`. Клиент реализован, но в текущей сборке нигде не инициализируется (см. примечание в конце). Для полноты — какие вызовы он делает:
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `{AUTH_HOST}/token/user/{userID}/` | Токен пользователя (Basic auth логин/пароль) |
| PUT | `{PDM_URL}/bundles/{bundleID}/marks` | Проставить маркировки бандлу |
### Workflows (`WORKFLOWS_HOST`, `AUTH_HOST`)
`internal/adapters/http/workflows/adapter.go` (resty). Используется воркером `split_pdf`.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{WORKFLOWS_HOST}/internal/v1/companies/{companyID}/workflows` | Создать workflow обработки/оптимизации PDF |
> Базовый URL resty-клиента установлен в `AUTH_HOST`, а адрес workflows подставляется полным (`WORKFLOWS_HOST`). В параметрах задач передаётся `django_host = AUTH_HOST`.
### Workspaces (`WORKSPACES_URL`)
`internal/adapters/http/workspaces/client.go`. Используется воркерами `copy`/`copyv2`.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{WORKSPACES_URL}/internal/v2/workspaces` | Создать рабочую область |
### System log (`SYSTEM_LOG_URL`)
`internal/adapters/http/system_log/client.go`. Используется воркерами `copy`, `copyv2`, `create_versions`, `create_versionsv2`.
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `{SYSTEM_LOG_URL}/api/v0/system_log` | Отправить пакет системных логов |
### Sarex backend (`SAREX_BACKEND_BASE_URL`)
`internal/adapters/sarexbackend/client.go` (resty). Используется HTTP-сервером при подписи (проверка MRPA). Токены проксируются из входящего запроса (`Authorization`, опц. `Identity`).
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `{SAREX_BACKEND_BASE_URL}/api/core/mrpa/{id}/` | Получить MRPA по id |
### Telegram Bot API (`TELEGRAM_TOKEN`, `TELEGRAM_ALERT_GROUP_ID`)
`internal/adapters/http/telegram/client.go` через `go-telegram-bot-api`. Отправка алертов в заданную группу при ошибках/паниках в задачах воркеров.
### RabbitMQ / AMQP (`AMQP_*`)
Подключение вида `amqp://{USER}:{PASSWORD}@{HOST}:{PORT}/{PATH_API}`.
| Адаптер | Назначение |
| --- | --- |
| `internal/adapters/amqp/markings` | Маркировка бандла и получение его хеш-суммы (RPC поверх временной очереди, `correlation_id`). Используется воркерами `markingsv2`/`copyv2` |
| `internal/adapters/amqp/rpc` | Общий RPC-клиент RabbitMQ (переподключение, вычисление хеш-сумм объектов) |
## Обработка ошибок
Каждый адаптер проверяет `StatusCode()` ответа и оборачивает не-`200 OK` в ошибку с телом ответа (`internal/errors`). Отдельно у sarexbackend маппинг: `404 → ErrNotFound`, `403 → ErrForbidden`, прочие → generic. Адаптеры flows и pdm выполняют ретраи с экспоненциальным backoff (до 5 попыток).
## Примечания
- **PDM-клиент не подключён.** `internal/adapters/http/pdm` реализован, а переменная `PDM_URL` присутствует в Helm-секрете, но `pdm.New(...)` не вызывается ни в одном бинарнике. Раздел оставлен для полноты; при фактическом использовании актуализируйте документ.
- Пути даны относительно базовых URL из окружения; итоговый URL = `<базовый адрес>` + `путь`.

298
apps/cde/openapi.yaml Normal file
View File

@ -0,0 +1,298 @@
openapi: 3.0.3
info:
title: CDE Orchestration API
version: "0.0.0"
description: |
HTTP API оркестратора **cde-orchestration-demo**
(`gitlab.com/sarex-team/cde-orchestration-demo`) — управление процессами
согласования/подписи документов в Camunda (Zeebe/Operate).
Сервис написан на Go (**Fiber v3**), точка входа — `cmd/http/main.go`,
сборка приложения — `internal/app/http/server.go`. Все ручки объявлены в
`internal/controller/http/v0` и смонтированы под префиксом `/api`.
### Аутентификация
Все эндпоинты проходят через middleware `pkg/http/middleware/auth.go`.
Токен передаётся заголовком `Authorization: Bearer <jwt>`. Поддерживаются
два режима:
1. **Sarex** (по умолчанию) — подпись JWT проверяется RSA public key из
переменной `PUBLIC_KEY`.
2. **Zitadel** — если передан дополнительный заголовок
`Identity: Bearer <jwt>`, полезная нагрузка берётся из метаданных
этого токена (`urn:zitadel:iam:user:metadata`); подпись основным
сервисом не проверяется.
При отсутствии/некорректности заголовков middleware возвращает `401`.
### Замечания
- Ручка `GET /api/process/{instance_key}` может вернуть `425 Too Early`,
если процесс есть в кеше, но ещё не создан в Camunda (идёт обработка).
- Тело ответов на запись (`process`, `sign`, `operate`) обычно пустое —
значим только HTTP-статус.
contact:
name: cde-orchestration-demo
url: https://gitlab.com/sarex-team/cde-orchestration-demo
servers:
- url: http://localhost:8080/api
description: Локальный запуск (Fiber, ADDRESS по умолчанию :8080)
- url: http://cde-svc.cde.svc.cluster.local/api
description: Внутрикластерный адрес (ClusterIP)
tags:
- name: infra
description: Служебные эндпоинты
- name: process
description: Процессы согласования/подписи
- name: sign
description: Отправка подписей в процесс
- name: operate
description: Управление определениями процессов (BPMN)
security:
- bearerAuth: []
paths:
/:
get:
tags: [infra]
summary: Проверка доступности
description: Возвращает 200 OK. Требует валидной авторизации (middleware).
operationId: root
responses:
"200":
description: OK
"401":
description: Не авторизован
/process/:
post:
tags: [process]
summary: Запустить процесс
description: |
Создаёт инстанс процесса согласования/подписи в Camunda по документам
из `payload`. Если для `flow_id` уже есть активный инстанс — вернётся
`400`.
operationId: createProcess
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProcessCreateRequest"
responses:
"202":
description: Процесс принят к обработке
"400":
description: Ошибка валидации или активный инстанс уже существует
"401":
description: Не авторизован
"500":
description: Внутренняя ошибка (ошибка создания инстанса в Camunda)
/process/{instance_key}:
get:
tags: [process]
summary: Получить состояние процесса
description: |
Возвращает текущее состояние процесса по `flow_id` (в пути — числовой
ключ). Логика: если запись есть в кеше, но нет активного инстанса в
Camunda — процесс ещё обрабатывается (`425`).
operationId: getProcess
parameters:
- name: instance_key
in: path
required: true
description: Числовой идентификатор (`flow_id`)
schema:
type: integer
format: uint64
responses:
"200":
description: Состояние процесса
content:
application/json:
schema:
$ref: "#/components/schemas/GetProcessInstanceResponse"
"401":
description: Не авторизован
"404":
description: Процесс не найден
"425":
description: Too Early — процесс ещё обрабатывается
"500":
description: Внутренняя ошибка
/sign/:
post:
tags: [sign]
summary: Отправить подписи в процесс
description: |
Публикует сообщение `signRequest` в процесс Camunda с подписями из
`payload`. Для элементов с `mrpa_id` предварительно проверяется доступ
через sarex-backend.
operationId: sign
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SignRequest"
responses:
"200":
description: Подписи приняты, сообщение отправлено в процесс
"401":
description: Отсутствует токен авторизации
"403":
description: Нет прав на MRPA
"422":
description: MRPA не найдена
"500":
description: Внутренняя ошибка
/operate/processes:
post:
tags: [operate]
summary: Загрузить определение процесса (BPMN)
description: |
Принимает BPMN-файл (multipart, поле `definition`) и деплоит его в
Zeebe.
operationId: deployProcessDefinition
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [definition]
properties:
definition:
type: string
format: binary
description: BPMN-файл определения процесса
responses:
"200":
description: Определение загружено
"400":
description: Файл не передан/некорректен
"401":
description: Не авторизован
"500":
description: Внутренняя ошибка деплоя
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT Sarex (проверяется по `PUBLIC_KEY`). Для режима Zitadel
дополнительно передаётся заголовок `Identity: Bearer <jwt>`.
schemas:
ProcessCreateRequest:
type: object
required: [flow_id, author_id]
description: Запрос на старт процесса (`internal/dto/http.go`).
properties:
flow_id:
type: integer
format: uint64
description: Внешний ID процесса (обязателен, != 0)
author_id:
type: integer
format: uint64
description: ID автора запроса (обязателен, != 0)
company_id:
type: integer
format: uint64
step_id:
type: integer
format: uint64
metadata:
type: object
additionalProperties: true
payload:
type: array
description: Документы для обработки (произвольные объекты)
items:
type: object
additionalProperties: true
overwrite_marks:
type: boolean
mode:
type: string
description: Режим работы ("original", "copy", "both")
use_signature:
type: boolean
create_copy_on_finish:
type: boolean
comment:
type: string
is_last_signer:
type: boolean
GetProcessInstanceResponse:
type: object
description: Состояние инстанса процесса (`internal/dto/http.go`).
properties:
status:
type: string
use_signature:
type: boolean
is_finished:
type: boolean
is_ready_for_sign:
type: boolean
is_last_signer:
type: boolean
create_copy_on_finish:
type: boolean
comment:
type: string
payload:
description: Полезная нагрузка процесса (структура зависит от процесса)
nullable: true
instance_key:
type: integer
format: uint64
SignRequest:
type: object
required: [flow_id, payload]
description: Запрос на подпись документов в процессе (`internal/dto/http.go`).
properties:
flow_id:
type: integer
format: uint64
metadata:
type: object
additionalProperties: true
payload:
type: array
items:
$ref: "#/components/schemas/SignRequestPayloadElem"
SignRequestPayloadElem:
type: object
properties:
bundle_id:
type: string
format: uuid
author_id:
type: integer
format: uint64
signature:
type: string
description: Сгенерированная подпись (помещается в p7s)
algorithm:
type: string
mrpa_id:
type: string
format: uuid
nullable: true
description: Если задан — проверяется доступ через sarex-backend

View File

@ -0,0 +1,31 @@
# Piccolo (обязательно для запуска)
PYTHONPATH=src
PICCOLO_CONF=db.config
# App
DEBUG=true
# HTTP app
HTTP_APP_HOST=0.0.0.0
HTTP_APP_PORT=8000
HTTP_APP_ROOT_PATH=""
HTTP_APP_WORKERS=1
HTTP_APP_ADMIN_ENABLE=true
# Database
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
# OpenTelemetry
OTEL_ENABLE=false
OTEL_URL=http://signoz-otel-collector-external.signoz.svc.cluster.local:4317
OTEL_SERVICE_NAME=checklists-backend.checklists-stage
OTEL_INSECURE=true
# Auth (JWT)
# При JWT_AUTH_ENABLE=false middleware отключён и используется дефолтный пользователь
JWT_AUTH_ENABLE=false
JWT_AUTH_PUBLIC_KEY=key

View File

@ -0,0 +1,171 @@
# Конфигурация проекта checklists-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/).
В отличие от единого класса настроек, конфигурация разбита на несколько независимых классов `BaseSettings`, у каждого — **свой** `env_prefix` (плоские имена, без вложенного разделителя):
| Класс | `env_prefix` | Раздел |
| --- | --- | --- |
| `Config` | *(нет префикса)* | `debug` |
| `HTTPAppConfig` | `HTTP_APP_` | Параметры HTTP-приложения/uvicorn |
| `DatabaseConfig` | `DATABASE_` | Подключение к PostgreSQL |
| `OTELConfig` | `OTEL_` | Трейсинг/логи OpenTelemetry |
| `JWTAuthConfig` | `JWT_AUTH_` | Аутентификация по JWT |
Подклассы подключаются к корневому `Config` как поля со значениями по умолчанию (`http_app`, `database`, `otel`, `jwt_auth`) и читают окружение в момент импорта. Отдельного конфиг-файла (yaml/toml) у приложения нет.
Особенности:
- **`.env` не загружается автоматически** — в `config.py` не задан `env_file`, зависимости `python-dotenv` нет. Файл `.env.template` — это шаблон; переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s.
- Помимо переменных приложения, для запуска нужны две инфраструктурные переменные Piccolo: `PYTHONPATH=src` и `PICCOLO_CONF=db.config` (заданы в `.env.template` и в `Dockerfile`).
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (`make run`) | Переменные окружения процесса. `.env.template` — шаблон, приложение его **не подхватывает** автоматически |
| Контейнер | `docker/http/Dockerfile` задаёт `PYTHONPATH`/`PICCOLO_CONF`; прочие переменные пробрасываются при запуске |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и `HELM_SET_ARGS` |
Способы запуска (`Makefile`):
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `make run` | `src/cmd/http/main.py``uvicorn` (factory `app.http:create_app`) | HTTP API |
| `make migrate` | `piccolo migrations forwards all` | Применение миграций БД |
| `make migrations` | `piccolo migrations new checklists --auto` | Генерация новой миграции |
| `make format` / `make format-check` | `ruff` | Форматирование/линт |
Порядок запуска в контейнере (`docker/http/entrypoint.sh`): сначала выполняются миграции (`piccolo migrations forwards all`), затем стартует приложение (`src/cmd/http/main.py`). Uvicorn запускается в режиме фабрики; `reload` включается при `DEBUG=true`, число воркеров — из `HTTP_APP_WORKERS`.
## Переменные приложения
В столбце «Переменная» указано полное имя (префикс + поле). Дефолт `—` означает, что значение обязательно.
### App (`Config`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DEBUG` | bool | `True` | Режим отладки. Влияет на `reload` uvicorn, логирование SQL-запросов Piccolo (`log_queries`/`log_responses`), а также на `production`-флаг и `debug` piccolo-admin |
### HTTP-приложение (`HTTP_APP_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HTTP_APP_HOST` | string | `0.0.0.0` | Адрес прослушивания |
| `HTTP_APP_PORT` | int | `8000` | Порт |
| `HTTP_APP_ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси; в k8s — `/checklists`) |
| `HTTP_APP_WORKERS` | int | `1` | Число воркеров uvicorn |
| `HTTP_APP_ADMIN_ENABLE` | bool | `True` | Монтировать ли piccolo-admin по пути `/admin/` |
### Database (`DATABASE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DATABASE_HOST` | string | `postgres` | Хост PostgreSQL |
| `DATABASE_PORT` | int | `5432` | Порт PostgreSQL |
| `DATABASE_NAME` | string | `postgres` | Имя базы данных |
| `DATABASE_USER` | string | `postgres` | Пользователь БД |
| `DATABASE_PASSWORD` | string | `postgres` | Пароль пользователя БД |
Подключение собирается в `src/db/config.py` (`PostgresEngine`). SSL-параметров в настройках нет; в prod TLS обеспечивается на уровне подключения/CA-сертификата (см. `docker/http/ca.crt`).
### OpenTelemetry (`OTEL_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `OTEL_ENABLE` | bool | `False` | Включить трейсинг/логи OTEL. При `True` подключаются `fastapi-otel-tools` и инструментирование FastAPI |
| `OTEL_URL` | string | `http://signoz-otel-collector-external.signoz.svc.cluster.local:4317` | Адрес OTLP-коллектора |
| `OTEL_SERVICE_NAME` | string | `checklists-backend.checklists-stage` | Имя сервиса в трейсах |
| `OTEL_INSECURE` | bool | `True` | Небезопасное (без TLS) подключение к коллектору |
> При `OTEL_ENABLE=true` `access_log` uvicorn отключается (логи идут через OTEL-обработчик).
### Auth (`JWT_AUTH_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `JWT_AUTH_ENABLE` | bool | `False` | Включить `JWTAuthMiddleware`. При `False` middleware не подключается, и в контекст подставляется дефолтный пользователь (для локальной разработки) |
| `JWT_AUTH_PUBLIC_KEY` | string | `key` | Публичный RSA-ключ для JWT (алгоритм `RS512`) |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения, но участвуют в запуске/сборке.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `PYTHONPATH` | `.env.template`, `Dockerfile` | Путь к исходникам (`src`) |
| `PICCOLO_CONF` | `.env.template`, `Dockerfile` | Путь к конфигу Piccolo (`db.config`) |
| `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`checklists-backend`) |
| `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile (`./docker/http/Dockerfile`) |
| `IMAGE_NAME` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Имя собираемого образа |
| `CHART_NAME` / `CHART_VERSION` / `RELEASE_NAME` | `.gitlab-ci.yml` | Параметры релиза Helm |
Базовый образ — `python:3.13-slim-bookworm`; менеджер зависимостей — `uv` (`uv sync --locked`). В образ добавляется CA-сертификат Yandex (`docker/http/ca.crt`).
## Переменные из Helm-чарта (`.helm/values.yaml`)
Сервис деплоится подключаемым чартом `universal-chart` (OCI-зависимость). Окружение выбирается ключом `universal-chart.global.env` (`stage`/`preprod`/`production`); для каждой переменной значение берётся из блока с ключом текущего окружения либо из `_default`.
Обычные значения (блок `envs`) переопределяют дефолты кода, в частности:
| Переменная | Значение в чарте |
| --- | --- |
| `HTTP_APP_ROOT_PATH` | `/checklists` |
| `HTTP_APP_WORKERS` | `3` |
| `HTTP_APP_ADMIN_ENABLE` | `true` |
| `DATABASE_PORT` | `6432` (PgBouncer) |
| `DATABASE_NAME` | `checklists_db` (stage), `checklists` (preprod/production) |
| `OTEL_ENABLE` | `true` |
| `OTEL_URL` | `http://otel-collector.opentelemetry-collector.svc.cluster.local:4317` |
| `OTEL_SERVICE_NAME` | `checklists-backend.proc` (stage), `…checklists-preprod`, `…checklists-prod` |
| `JWT_AUTH_ENABLE` | `true` |
| `DEBUG` | `false` |
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `DATABASE_USER` | `checklists-postgresql-secret` (stage) / `ya-pg-secret` (preprod, production) | `user` |
| `DATABASE_PASSWORD` | `checklists-postgresql-secret` / `ya-pg-secret` | `password` |
| `DATABASE_HOST` | `checklists-postgresql-secret` / `ya-pg-secret` | `host` |
| `JWT_AUTH_PUBLIC_KEY` | `jwt-secret` | `public-key` |
Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики — 1 по умолчанию, 2 в production, ресурсы), `image.*`, `service.*` (ClusterIP, порт `80``8000`). Проверки `liveness`/`readiness` отключены.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline.yaml`, `common-security-scan.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace |
| --- | --- | --- |
| ветка `stage` | `stage` | `proc` |
| ветка `master` | `preprod` | `checklists-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `checklists-prod` |
Ключевые переменные пайплайна: `SERVICE_NAME`, `DOCKERFILE_PATH`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (проброс `image.name`, `global.env`, метаданных коммита). Job `lint` прогоняет `ruff check`/`ruff format --check` на образе `uv`.
## Замечания и потенциальные проблемы
- Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение вручную либо задавать в манифестах.
- Аутентификация JWT в коде выполняет разбор токена с `options={"verify_signature": False}` в обоих режимах (sarex-backend и Zitadel) — подпись фактически не проверяется на уровне приложения, доверие обеспечивается сетевым слоем (Istio). При `JWT_AUTH_ENABLE=false` middleware не подключается и используется дефолтный пользователь из `entity/context.py`.
- Внутренние эндпоинты (`/internal/*`) аутентификации на уровне приложения не требуют.
- SSL-настроек подключения к БД в коде нет; в prod используется PgBouncer (`DATABASE_PORT=6432`) и CA-сертификат, вшитый в образ.
- Piccolo-admin доступен по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`; в режиме `DEBUG=false` он поднимается в `production`-режиме.
## Минимальный набор для локального запуска
Нужен доступный PostgreSQL. Помимо `PYTHONPATH=src` и `PICCOLO_CONF=db.config`, для запуска достаточно значений по умолчанию — обязательных переменных без дефолта нет. Практически стоит задать:
- `DEBUG` (`true` локально)
- `HTTP_APP_HOST`, `HTTP_APP_PORT`
- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`
- `JWT_AUTH_ENABLE` (`false` для локальной разработки — тогда используется дефолтный пользователь)
- `OTEL_ENABLE` (`false` локально)
Готовые значения-примеры приведены в `.env.example`.

View File

@ -0,0 +1,963 @@
openapi: 3.0.3
info:
title: Checklists
version: "0.1.0"
description: |
REST API сервиса **checklists-backend** — управление чек-листами
(`Checklist`) и их результатами (`ChecklistResult`).
Сервис написан на Python (**FastAPI** + ORM **Piccolo**). Приложение
собирается фабрикой `create_app` в `src/app/http.py`. Роутинг состоит из
двух групп:
- публичный API — префикс `/api/v1` (`controller/http/api`);
- внутренний API — префикс `/internal/v1` (`controller/http/internal_api`),
предназначен для вызовов внутри кластера (через ingress не публикуется).
Интерактивная документация (ReDoc) доступна по `/docs/`, схема —
по `/openapi.json/` (с учётом `root_path`). Админ-панель Piccolo монтируется
по `/admin/` при `HTTP_APP_ADMIN_ENABLE=true`.
### Аутентификация
Аутентификация включается флагом `JWT_AUTH_ENABLE`. При включённом
`JWTAuthMiddleware` (`controller/http/middlewares.py`) публичные эндпоинты
требуют заголовок `Authorization: Bearer <jwt>`. Поддерживаются два режима:
1. **Zitadel** — если передан дополнительный заголовок `identity`
(`Identity <jwt>`), полезная нагрузка (`user_id`, `company_ids`) берётся
из этого токена (`urn:zitadel:iam:user:metadata`).
2. **sarex-backend** — если заголовка `identity` нет, разбирается основной
токен (алгоритм `RS512`, ключ `JWT_AUTH_PUBLIC_KEY`).
В обоих режимах разбор выполняется с `verify_signature=False` — подпись на
уровне приложения не проверяется, доверие обеспечивается сетевым слоем.
Внутренние эндпоинты (`/internal/*`) и пути `/docs/`, `/openapi.json/`,
`/admin/*` аутентификацию пропускают. При `JWT_AUTH_ENABLE=false` middleware
не подключается и используется дефолтный пользователь.
### Пагинация
Списочные ответы используют пагинацию limit/offset и оборачиваются в
`PaginatedResponse` — `{ count, result }`, где `count` — число объектов в
текущем ответе, `result` — сами объекты. Параметры: `limit` (по умолчанию
`100`), `offset` (по умолчанию `0`), сортировка — `order_by`/`ascending`.
### Обработка ошибок
Доменные ошибки (`controller/http/errors.py`) возвращаются как
`application/json` с телом `{ "detail": "<текст>" }`. Маппинг:
- `ResourceNotPermittedError` → **403**;
- `ResourceNotFoundError` → **404**;
- `StateConflictError` → **409**;
- `ChecklistResultValidationError` → **422**;
- прочее → **500**.
Ошибки валидации тела/параметров запроса (Pydantic) отдаются FastAPI в
стандартном формате `422` (`HTTPValidationError`). Все публичные эндпоинты
(`/api/*`) при невалидном/отсутствующем токене возвращают `401`.
contact:
name: checklists-backend
url: https://gitlab/proc/checklists-backend
servers:
- url: https://api.sarex.io/checklists
description: Production (ingress, root_path=/checklists)
- url: https://stage-api.sarex.io/checklists
description: Stage (ingress, root_path=/checklists)
- url: http://checklists-backend-service.proc.svc.cluster.local
description: Внутрикластерный адрес (ClusterIP, порт 80 → 8000). Единственный способ достучаться до /internal/v1
- url: http://localhost:8000
description: Локальный запуск (Uvicorn, порт по умолчанию 8000)
tags:
- name: Checklists
description: Чек-листы — создание, просмотр, поиск, удаление
- name: Checklist results
description: Результаты чек-листов — создание, просмотр, обновление, удаление
- name: internal
description: Внутренние эндпоинты (только внутри кластера)
security:
- bearerAuth: []
paths:
# ==========================================================================
# Checklists
# ==========================================================================
/api/v1/checklists/:
post:
tags: [Checklists]
summary: Создать чек-лист
operationId: createChecklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistCreate'
responses:
'201':
description: Созданный чек-лист
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistReadFull'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/ValidationError' }
get:
tags: [Checklists]
summary: Список чек-листов
operationId: listChecklists
parameters:
- { $ref: '#/components/parameters/OrderByChecklist' }
- { $ref: '#/components/parameters/Ascending' }
- { $ref: '#/components/parameters/Limit' }
- { $ref: '#/components/parameters/Offset' }
- name: company_id
in: query
required: false
description: Фильтр по ID компаний
schema:
type: array
nullable: true
items:
type: integer
minimum: 1
responses:
'200':
description: Страница чек-листов (компактное представление)
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact'
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/v1/checklists/filter/:
post:
tags: [Checklists]
summary: Список чек-листов (фильтры в теле)
description: Аналог `GET /api/v1/checklists/`, но фильтры и пагинация передаются в теле запроса.
operationId: filterChecklists
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistFiltersWithPagination'
responses:
'200':
description: Страница чек-листов (компактное представление)
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResponse_ChecklistReadCompact'
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/v1/checklists/{instance_id}/:
get:
tags: [Checklists]
summary: Чек-лист по id
operationId: getChecklist
parameters:
- { $ref: '#/components/parameters/InstanceId' }
responses:
'200':
description: Чек-лист (полное представление)
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistReadFull'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Checklists]
summary: Удалить чек-лист
operationId: deleteChecklist
parameters:
- { $ref: '#/components/parameters/InstanceId' }
responses:
'204':
description: Чек-лист удалён
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
# ==========================================================================
# Checklist results
# ==========================================================================
/api/v1/results/:
post:
tags: [Checklist results]
summary: Создать результат чек-листа
description: |
Создаёт результат по `checklist_id`. Данные чек-листа переносятся бэком
автоматически; в теле передаются метаданные и список значений инпутов
(`input_values`).
operationId: createChecklistResult
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultCreate'
responses:
'201':
description: Созданный результат
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultRead'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/ValidationError' }
get:
tags: [Checklist results]
summary: Список результатов чек-листов
operationId: listChecklistResults
parameters:
- { $ref: '#/components/parameters/OrderByChecklistResult' }
- { $ref: '#/components/parameters/Ascending' }
- { $ref: '#/components/parameters/Limit' }
- { $ref: '#/components/parameters/Offset' }
- { $ref: '#/components/parameters/FilterResultId' }
- { $ref: '#/components/parameters/FilterChecklistId' }
- { $ref: '#/components/parameters/FilterCreatorId' }
- { $ref: '#/components/parameters/FilterIsDraft' }
- { $ref: '#/components/parameters/FilterIsLocked' }
responses:
'200':
description: Страница результатов
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead'
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/v1/results/{instance_id}/:
get:
tags: [Checklist results]
summary: Результат чек-листа по id
operationId: getChecklistResult
parameters:
- { $ref: '#/components/parameters/InstanceId' }
responses:
'200':
description: Результат чек-листа
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultRead'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
patch:
tags: [Checklist results]
summary: Обновить результат чек-листа
description: Частичное обновление значений инпутов и флага черновика.
operationId: updateChecklistResult
parameters:
- { $ref: '#/components/parameters/InstanceId' }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultPartialUpdate'
responses:
'200':
description: Обновлённый результат
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultRead'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Checklist results]
summary: Удалить результат чек-листа
operationId: deleteChecklistResult
parameters:
- { $ref: '#/components/parameters/InstanceId' }
responses:
'204':
description: Результат удалён
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
# ==========================================================================
# Internal
# ==========================================================================
/internal/v1/results/:
get:
tags: [internal]
summary: Список результатов (внутренний)
description: Внутрикластерный эндпоинт. Аутентификация на уровне приложения не выполняется.
operationId: internalListChecklistResults
security: []
parameters:
- { $ref: '#/components/parameters/OrderByChecklistResult' }
- { $ref: '#/components/parameters/Ascending' }
- { $ref: '#/components/parameters/Limit' }
- { $ref: '#/components/parameters/Offset' }
- { $ref: '#/components/parameters/FilterResultId' }
- { $ref: '#/components/parameters/FilterChecklistId' }
- { $ref: '#/components/parameters/FilterCreatorId' }
- { $ref: '#/components/parameters/FilterIsDraft' }
- { $ref: '#/components/parameters/FilterIsLocked' }
responses:
'200':
description: Страница результатов
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResponse_ChecklistResultRead'
'422': { $ref: '#/components/responses/ValidationError' }
/internal/v1/results/lock:
patch:
tags: [internal]
summary: Массовое обновление блокировки результатов
description: Устанавливает флаг `is_locked` для списка результатов по их id. Внутрикластерный эндпоинт.
operationId: internalBulkUpdateLock
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChecklistResultBulkLockingPartialUpdate'
responses:
'204':
description: Флаги блокировки обновлены
'422': { $ref: '#/components/responses/ValidationError' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT в заголовке `Authorization: Bearer <token>`. Алгоритм `RS512`,
ключ `JWT_AUTH_PUBLIC_KEY`. Разбор выполняется без проверки подписи
(`verify_signature=False`).
identityToken:
type: apiKey
in: header
name: identity
description: |
Опциональный заголовок `identity` (`Identity <jwt>`) для режима Zitadel.
При его наличии полезная нагрузка (`user_id`, `company_ids`) берётся из
этого токена.
parameters:
InstanceId:
name: instance_id
in: path
required: true
schema:
type: integer
Limit:
name: limit
in: query
required: false
description: Максимальное количество объектов
schema:
type: integer
default: 100
Offset:
name: offset
in: query
required: false
description: Количество пропущенных объектов
schema:
type: integer
default: 0
Ascending:
name: ascending
in: query
required: false
description: Сортировка по возрастанию
schema:
type: boolean
default: true
OrderByChecklist:
name: order_by
in: query
required: false
description: Поле для сортировки
schema:
type: string
enum: [id]
default: id
OrderByChecklistResult:
name: order_by
in: query
required: false
description: Поле для сортировки
schema:
type: string
enum: [id]
default: id
FilterResultId:
name: id
in: query
required: false
description: Фильтр по ID результата
schema:
type: array
nullable: true
items:
type: integer
minimum: 1
FilterChecklistId:
name: checklist_id
in: query
required: false
description: Фильтр по ID чек-листа
schema:
type: array
nullable: true
items:
type: integer
minimum: 1
FilterCreatorId:
name: creator_id
in: query
required: false
description: Фильтр по ID создателя результата
schema:
type: array
nullable: true
items:
type: integer
minimum: 1
FilterIsDraft:
name: is_draft
in: query
required: false
description: Признак «чернового» результата
schema:
type: boolean
nullable: true
FilterIsLocked:
name: is_locked
in: query
required: false
description: Признак блокировки результата
schema:
type: boolean
nullable: true
responses:
Unauthorized:
description: Токен не предоставлен или невалиден
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPError'
Forbidden:
description: Недостаточно прав
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPError'
NotFound:
description: Запрошенный ресурс не найден
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPError'
Conflict:
description: Конфликт состояния
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPError'
ValidationError:
description: |
Ошибка валидации тела/параметров запроса (FastAPI/Pydantic) либо
доменная ошибка валидации результата (`{ detail }`).
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
schemas:
# ---- Общие ----
HTTPError:
type: object
properties:
detail:
type: string
description: Описание ошибки
required: [detail]
HTTPValidationError:
type: object
properties:
detail:
type: array
items:
$ref: '#/components/schemas/ValidationError'
ValidationError:
type: object
properties:
loc:
type: array
items:
anyOf:
- type: string
- type: integer
msg:
type: string
type:
type: string
required: [loc, msg, type]
# ---- Ограничения инпутов ----
InputChoiceOption:
type: object
properties:
id:
type: string
format: uuid
description: Уникальный идентификатор опции
name:
type: string
description: Название опции
example: "Да"
order:
type: integer
description: Порядковый номер опции
example: 1
color:
type: string
nullable: true
description: Цвет опции (hex или именованный html-цвет)
example: "#00aa00"
selected_tip:
type: string
nullable: true
description: Заметка при выборе опции
example: 'Рекомендуется добавить комментарий при ответе "Нет"'
alt_name:
type: string
nullable: true
description: Название опции для записи в историю
example: "Одобрено"
required: [id, name, order, selected_tip]
InputChoiceConstraints:
type: object
properties:
type:
type: string
enum: [choice]
options:
type: array
nullable: true
description: Список опций для выбора
items:
$ref: '#/components/schemas/InputChoiceOption'
required: [type, options]
InputStringConstraints:
type: object
properties:
type:
type: string
enum: [string]
min_length:
type: integer
minimum: 0
default: 0
description: Минимально допустимое количество символов
max_length:
type: integer
minimum: 1
default: 1000
description: Максимально допустимое количество символов
required: [type]
InputConstraints:
oneOf:
- $ref: '#/components/schemas/InputChoiceConstraints'
- $ref: '#/components/schemas/InputStringConstraints'
discriminator:
propertyName: type
mapping:
choice: '#/components/schemas/InputChoiceConstraints'
string: '#/components/schemas/InputStringConstraints'
# ---- Чек-лист (создание) ----
ChecklistInputCreate:
type: object
properties:
name:
type: string
maxLength: 1024
description: Название
example: "Комментарий"
order:
type: integer
description: Порядковый номер
is_required:
type: boolean
description: Обязательное ли поле для заполнения
constraints:
$ref: '#/components/schemas/InputConstraints'
required: [name, order, is_required, constraints]
ChecklistItemCreate:
type: object
properties:
description:
type: string
maxLength: 8192
description: Описание шага
order:
type: integer
description: Порядковый номер
inputs:
type: array
description: Список элементов ввода
items:
$ref: '#/components/schemas/ChecklistInputCreate'
required: [description, order, inputs]
ChecklistCreate:
type: object
properties:
name:
type: string
maxLength: 250
description: Название
example: "Чек-лист проверки документов"
description:
type: string
maxLength: 8192
description: Описание чек-листа
company_id:
type: integer
minimum: 1
description: ID компании
example: 1
items:
type: array
description: Список шагов чек-листа
items:
$ref: '#/components/schemas/ChecklistItemCreate'
required: [name, description, company_id, items]
# ---- Чек-лист (чтение) ----
ChecklistInputRead:
type: object
properties:
id:
type: integer
example: 1
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
name:
type: string
maxLength: 1024
order:
type: integer
is_required:
type: boolean
constraints:
$ref: '#/components/schemas/InputConstraints'
required: [id, created_at, updated_at, name, order, is_required, constraints]
ChecklistItemRead:
type: object
properties:
id:
type: integer
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
description:
type: string
maxLength: 8192
order:
type: integer
inputs:
type: array
description: Список элементов ввода
items:
$ref: '#/components/schemas/ChecklistInputRead'
required: [id, created_at, updated_at, description, order, inputs]
ChecklistReadCompact:
type: object
properties:
id:
type: integer
example: 1
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
name:
type: string
description:
type: string
company_id:
type: integer
minimum: 1
required: [id, created_at, updated_at, name, description, company_id]
ChecklistReadFull:
allOf:
- $ref: '#/components/schemas/ChecklistReadCompact'
- type: object
properties:
items:
type: array
description: Список шагов чек-листа
items:
$ref: '#/components/schemas/ChecklistItemRead'
required: [items]
ChecklistFiltersWithPagination:
type: object
properties:
order_by:
type: string
enum: [id]
default: id
ascending:
type: boolean
default: true
limit:
type: integer
default: 100
offset:
type: integer
default: 0
company_id:
type: array
nullable: true
description: Фильтр по ID компаний
items:
type: integer
minimum: 1
# ---- Результаты чек-листов ----
ChecklistResultInputCreate:
type: object
properties:
input_id:
type: integer
description: ID инпута, для которого устанавливается значение
example: 1
value:
description: 'Значение (тип зависит от инпута: id опции для choice, строка для string)'
nullable: true
example: "Да"
required: [input_id, value]
ChecklistResultCreate:
type: object
properties:
entity_type:
type: string
description: Сущность, для которой создан результат
example: "review"
entity_id:
type: string
description: ID сущности, для которой создан результат
example: "1"
checklist_id:
type: integer
description: ID чек-листа
example: 1
accessible_by:
type: array
description: SA ID роли/места/пользователя, которым доступен результат
items:
type: string
format: uuid
is_draft:
type: boolean
description: Является ли результат черновым
input_values:
type: array
description: Список устанавливаемых значений
items:
$ref: '#/components/schemas/ChecklistResultInputCreate'
required: [entity_type, entity_id, checklist_id, accessible_by, is_draft, input_values]
ChecklistResultPartialUpdate:
type: object
properties:
input_values:
type: array
description: Список устанавливаемых значений
items:
$ref: '#/components/schemas/ChecklistResultInputCreate'
is_draft:
type: boolean
description: Является ли результат черновым
required: [input_values, is_draft]
ChecklistResultInput:
type: object
properties:
input_id:
type: integer
description: ID инпута, для которого создан результат
name:
type: string
description: Название
order:
type: integer
description: Порядковый номер
value:
type: string
nullable: true
description: Введённое значение (строка или UUID выбранной опции)
value_text:
type: string
nullable: true
description: Текстовое представление введённого значения
color:
type: string
nullable: true
description: Цвет значения
required: [input_id, name, order, value, value_text, color]
ChecklistResultItem:
type: object
properties:
description:
type: string
description: Описание шага
order:
type: integer
description: Порядковый номер
inputs:
type: array
description: Список введённых значений
items:
$ref: '#/components/schemas/ChecklistResultInput'
required: [description, order, inputs]
ChecklistResultRead:
type: object
properties:
id:
type: integer
example: 1
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
entity_type:
type: string
example: "review"
entity_id:
type: string
example: "1"
checklist_id:
type: integer
accessible_by:
type: array
items:
type: string
format: uuid
is_draft:
type: boolean
company_id:
type: integer
description: ID компании
is_locked:
type: boolean
description: Заблокирован ли результат для изменений
items:
type: array
description: Список шагов чек-листа
items:
$ref: '#/components/schemas/ChecklistResultItem'
required:
- id
- created_at
- updated_at
- entity_type
- entity_id
- checklist_id
- accessible_by
- is_draft
- company_id
- is_locked
- items
ChecklistResultBulkLockingPartialUpdate:
type: object
properties:
ids:
type: array
description: Список id результатов, которым нужно обновить флаг
items:
type: integer
example: [1, 2, 3]
is_locked:
type: boolean
description: Заблокирован ли результат для изменений
required: [ids, is_locked]
# ---- Пагинация ----
PaginatedResponse_ChecklistReadCompact:
type: object
properties:
count:
type: integer
description: Количество объектов
example: 1
result:
type: array
description: Объекты
items:
$ref: '#/components/schemas/ChecklistReadCompact'
required: [count, result]
PaginatedResponse_ChecklistResultRead:
type: object
properties:
count:
type: integer
description: Количество объектов
example: 1
result:
type: array
description: Объекты
items:
$ref: '#/components/schemas/ChecklistResultRead'
required: [count, result]

View File

@ -0,0 +1,54 @@
# Пример переменных окружения для comparisons-backend.
# Значения разбираются пакетом kelseyhightower/envconfig (config/config.go, FromEnv).
# Префикса нет — имена переменных используются как есть.
# Приложение НЕ загружает .env автоматически: переменные нужно экспортировать
# в окружение (для локального запуска см. .docker/.env и docker-compose).
# API
API_ADDRESS=0.0.0.0:8080
# Database (PostgreSQL)
POSTGRES_ADDRESS=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=comparisons
POSTGRES_POOL_SIZE=10
# TLS-подключение к БД. При ENABLE_SSL=1 используется YC-PG-CERTIFICATE как CA.
ENABLE_SSL=0
DB_CERT_PATH=/home/user/.postgresql/root.crt
YC-PG-CERTIFICATE=
# Внешние сервисы (внутрикластерные адреса)
DOCUMENTATION_URL=http://documentations-service.documentations-stage/
EXTERNAL_DOCUMENTATION_URL=https://stage-api.sarex.io/documentations
# Хранилище файлов PDM (multistorage, config/storage.go). Без него сервис
# стартует, но с предупреждением — PDM-хранилище будет недоступно.
DOCUMENTATION_FILESTREAM_URL=http://documentations-filestream-service.documentations-stage/
WORKFLOW_URL=http://workflows-service.processing-stage/
WORKSPACE_URL=http://workspaces-service.workspaces-stage/
EXTERNAL_WORKSPACE_URL=
COMPARISON_URL=http://comparisons-backend-service.comparisons-stage/
WORKFLOW_IMAGES_VERSION=develop
BIM_V2_INTERNAL_URL=http://bim-backend-v2-service.bim-api-stage/
# Используется клиентом workflow (pdf2pdf) как django_host в параметрах задачи.
DJANGO_HOST=https://stage.sarex.io
# Comparisons
# Ограничение параллелизма ABAP-сравнения (0 — без ограничения).
ABAP_FIXED_CONC=0
# Sentry / окружение
ENVIRONMENT=stage
SENTRY_DSN=
SENTRY_DEBUG=0
# Отладка
# Логировать SQL-запросы (go-pg query hook).
ENABLE_SQL_QUERY=0
# --- Дополнительно (используются только при локальном запуске / внешними
# --- библиотеками, кодом config.Config напрямую не читаются) ---
# S3_SERVICE_ACCOUNT=/etc/sarex/yc_s3_doc_account.json
# DJANGO_ORIGINATOR=docs_local
# NAMESPACE=local

View File

@ -0,0 +1,148 @@
# Конфигурация проекта comparisons-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса `comparisons-backend` (Go).
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` функцией `FromEnv()` через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (структура `Config`).
Особенности разбора:
- **префикса нет**`envconfig.Process("", &cfg)` вызывается с пустым префиксом, поэтому имена переменных совпадают с тегами `envconfig:"..."` (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`);
- **вложенности нет** — все переменные плоские (без разделителя секций);
- отдельные значения читаются напрямую через `os.Getenv` в обход структуры `Config`: `DOCUMENTATION_FILESTREAM_URL` (`config/storage.go`) и `DJANGO_HOST` (`clients/workflow_cli/pdf2pdf.go`).
Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение самому либо задавать через `--env`/`env_file` (docker-compose).
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. Собирается через `make api` (`go install ./cmd/api`, `./cmd/migrations`) |
| Локально (контейнеры) | `.docker/.env` + `.docker/docker-compose.yml` (`make docker`). Postgres поднимается из этого же compose |
| Kubernetes (Helm, репозиторий бэкенда) | `.helm/values-<env>.yaml`: блок `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблон `.helm/templates/api.yaml` |
| Kubernetes (Kustomize, этот репозиторий infra) | `apps/comparisons/base` + оверлеи; env задаются в `base/backend-deployment.yaml` и патчах оверлеев. **Схема переменных здесь отличается — см. раздел ниже** |
| CI/CD (GitLab) | `.gitlab-ci.yml`: подключает шаблоны `generic/common-ci` (stage/preprod/prod), job `unit-tests` (образ `golang:1.21`) |
Способы запуска процессов (`cmd/*`):
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `api` (`make api`) | `cmd/api/main.go` | HTTP API (gorilla/mux). Перед стартом настраивает Sentry и подключение к Postgres |
| `migrations` | `cmd/migrations/main.go` | Миграции БД (`robinjoseph08/go-pg-migrations`). Создание: `go run ./cmd/migrations/main.go create <name>` |
## Переменные приложения
Дефолт `—` означает, что явного значения по умолчанию нет (пустая строка / нулевое значение типа Go). Обязательность отдельных переменных проверяется в рантайме при создании клиентов.
### API
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `API_ADDRESS` | string | — | Адрес и порт прослушивания HTTP-сервера (напр. `0.0.0.0:8080`) |
### Database (PostgreSQL)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL |
| `POSTGRES_PORT` | string | — | Порт PostgreSQL |
| `POSTGRES_USER` | string | — | Пользователь БД |
| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД |
| `POSTGRES_DB` | string | — | Имя базы данных |
| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (в коде `main.go` пул жёстко равен `30`; переменная читается, но фактически используется значение из кода) |
| `ENABLE_SSL` | bool | `false` | Подключение к БД по TLS. При `true` используется `YC-PG-CERTIFICATE` как корневой сертификат, `ServerName` = `POSTGRES_ADDRESS` |
| `DB_CERT_PATH` | string | — | Путь к CA-сертификату PostgreSQL (используется инфраструктурой; в Helm — смонтированный файл) |
| `YC-PG-CERTIFICATE` | string | — | Содержимое CA-сертификата для TLS-подключения к БД. Имя с дефисами не соответствует остальным (`envconfig` допускает произвольный тег) |
### Внешние сервисы
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DOCUMENTATION_URL` | string | — | Внутренний URL сервиса документаций (обязателен для `documentation_cli`) |
| `EXTERNAL_DOCUMENTATION_URL` | string | — | Внешний URL сервиса документаций (обязателен для `documentation_cli`) |
| `DOCUMENTATION_FILESTREAM_URL` | string | — | URL PDM-хранилища файлов (`config/storage.go`). Если не задан — сервис стартует, но PDM-хранилище недоступно (лог-warning) |
| `WORKFLOW_URL` | string | — | Внутренний URL сервиса workflow (обязателен для `workflow_cli`) |
| `WORKSPACE_URL` | string | — | Внутренний URL сервиса workspace (обязателен для `workspace_cli`) |
| `EXTERNAL_WORKSPACE_URL` | string | — | Внешний URL сервиса workspace |
| `COMPARISON_URL` | string | — | URL самого сервиса сравнений (обязателен для `workflow_cli`) |
| `WORKFLOW_IMAGES_VERSION` | string | — | Версия/тег образов задач workflow (обязателен для `workflow_cli`) |
| `BIM_V2_INTERNAL_URL` | string | — | Внутренний URL BIM API v2 |
| `DJANGO_HOST` | string | — | Хост Django (LK). Передаётся как `django_host` в параметры задачи pdf2pdf (`clients/workflow_cli/pdf2pdf.go`) |
### Comparisons
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ABAP_FIXED_CONC` | uint64 | `0` | Ограничение параллелизма ABAP-сравнения (`0` — без ограничения) |
### Sentry и окружение
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENVIRONMENT` | string | — | Окружение развёртывания (`stage`/`preprod`/`prod`); передаётся в Sentry |
| `SENTRY_DSN` | string | — | DSN Sentry |
| `SENTRY_DEBUG` | bool | `false` | Debug-режим Sentry |
### Отладка
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы (query hook go-pg) |
### Дополнительные переменные (не читаются `config.Config`)
Присутствуют в `.docker/.env` для локального запуска или используются внешними библиотеками (`gotools`), но напрямую структурой `Config` не разбираются:
| Переменная | Где встречается | Назначение |
| --- | --- | --- |
| `S3_SERVICE_ACCOUNT` | `.docker/.env` | Путь к JSON сервисного аккаунта S3 |
| `DJANGO_ORIGINATOR` | `.docker/.env` | Ориджинатор для интеграции с Django |
| `NAMESPACE` | `.docker/.env` | Логическое пространство имён для локального запуска |
| `API_ADDRESS_FILE` | `.helm/values-*.yaml` | Адрес file-варианта API (задан в Helm, кодом не используется) |
## Переменные из Helm-чарта (`.helm/values-<env>.yaml`)
Обычные значения задаются в блоке `envs` для каждого окружения (`stage`/`preprod`/`production`) и содержат те же переменные приложения, что описаны выше (различаются адресами БД/сервисов, `WORKFLOW_IMAGES_VERSION`, доменом Django и т.п.).
Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) |
| --- | --- | --- |
| `POSTGRES_USER` | `ya-pg-secret` | `username` |
| `POSTGRES_PASSWORD` | `ya-pg-secret` | `password` |
| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `certificate` |
Прочие значения чарта (не переменные приложения): `api.*` (имя, образ, порт, реплики, ресурсы, ingress `api_host`/`api_host_prefix`/`api_path`/`internal_path`, `permitted_ns`), `version`, `imagePullSecrets`.
## Развёртывание через Kustomize (этот репозиторий, `apps/comparisons`)
Структура: `base` (общие манифесты) и оверлеи `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`. Секреты БД и публичный JWT-ключ подтягиваются из **HashiCorp Vault** (аннотации `vault.hashicorp.com/*` в `base/backend-deployment.yaml`), а не из k8s-секретов.
> **Важно: расхождение схем переменных.** Манифест `base/backend-deployment.yaml` использует другой (более новый) набор имён переменных, чем Go-код из `comparisons-backend` (`config/config.go`): напр. `HTTP_PORT`, `LOGGER_LOG_LEVEL`, `DATABASE_NAME`, `DOCUMENTATIONS_INTERNAL_HOST`, `DOCUMENTATIONS_EXTERNAL_HOST`, `WORKFLOWS_HOST`, `WORKFLOWS_DJANGO_HOST`, `WORKFLOWS_BIMV2_INTERNAL_HOST`, `WORKSPACES_HOST`, `EAV_HOST`, `APP_NAME`, `AUTH_PUBLIC_KEY`, `WORKFLOWS_CONFIG_FILEPATH` и др., а также `/ping` в health-проверках и образ `comparisons_backend_prod`. Такой схемы нет в Go-репозитории. Перед использованием этих файлов стоит убедиться, какой именно образ бэкенда деплоится оверлеем: если это Go-сервис из `comparisons-backend`, набор env нужно привести к именам из таблиц выше.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` и переключает окружение по ветке/тегу:
| Условие | Шаблон | Окружение |
| --- | --- | --- |
| ветка `stage` | `gitlab-ci/comparisons-backend/.gitlab-ci-stage.yml` | stage |
| ветка `master` | `gitlab-ci/comparisons-backend/.gitlab-ci-preprod.yml` | preprod |
| тег (`CI_COMMIT_TAG`) | `gitlab-ci/comparisons-backend/.gitlab-ci-prod.yml` | prod |
Стадии: `prebuild-secscan`, `dependencies-build`, `unittest`, `build`, `state-update`, `deploy`. Job `unit-tests` (образ `golang:1.21`) выполняет `make unit-tests`.
## Минимальный набор для локального запуска
Postgres поднимается через `.docker/docker-compose.yml` (`make docker`). Минимально необходимо задать:
- `API_ADDRESS`
- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL` (`0` локально)
- `DOCUMENTATION_URL`, `EXTERNAL_DOCUMENTATION_URL`, `DOCUMENTATION_FILESTREAM_URL`
- `WORKFLOW_URL`, `WORKSPACE_URL`, `COMPARISON_URL`, `WORKFLOW_IMAGES_VERSION`
- `BIM_V2_INTERNAL_URL`, `DJANGO_HOST`
- `ENVIRONMENT`; при использовании Sentry — `SENTRY_DSN`
- по желанию: `ENABLE_SQL_QUERY` (`1` для отладки SQL), `ABAP_FIXED_CONC`
Готовые значения-примеры приведены в `.env.example`.

View File

@ -0,0 +1,77 @@
# Эндпоинты, с которыми взаимодействует comparisons-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `comparisons-frontend`).
## Как устроено взаимодействие
Все запросы описаны декларативно в реестре `module/networking/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`:
- `service` — логическое имя сервиса (`EServices`, см. таблицу хостов ниже);
- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`);
- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query);
- `body(args)` — опционально, формирование тела запроса.
Запрос выполняется единой функцией `fetch(endpoint, params)`, которая через `httpService` (`module/httpService/httpService.ts`, поверх `@sarex-team/sdk-js` + `axios`) отправляет запрос на базовый хост сервиса. Базовый хост подставляется `resolveHost(service)` из `module/httpService/hosts.ts` в зависимости от `buildEnv` (`__BUILD_ENV__`, задаётся сборкой; по умолчанию `prod`). Результат возвращается как `{ resp }` либо `{ errMessage }`.
## Базовые хосты по сервисам и окружениям
Значения из `module/httpService/hosts.ts` (`allHosts`). Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта. Хосты берутся из `@sarex-team/sdk-js` (`resolveHost`).
| Сервис (`EServices`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `comparisons` | Сервис сравнений (comparisons-backend) | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` |
| `documentations` | Сервис документации (диски, документы) | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` |
| `sarexApi` | Gateway/API Sarex (`/gateway`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `sarex` | Локальный сервис данных (`/api/core`) | `""` (относительные пути) | `""` |
| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` |
| `bimv2` | BIM API v2 | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определены окружения `local`, `preprod` и `contour`. В `local` сервисы проксируются на `https://localhost:9000/sarex-backend` и `https://localhost:9000/sarex-api-backend/*`. В `contour` используются относительные пути (`/comparisons`, `/documentations`, `/bimv2`, `/workflows`) для изолированного контура. В `preprod``https://api.preprod.sarex.io/*`.
## Эндпоинты по сервисам
### `comparisons` — Сервис сравнений
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getTypes` | GET | `/api/v1/types` | Справочник типов сравнений и их параметров |
| `createComparison` | POST | `/api/v1/comparisons` | Создать сравнение (тело — параметры сравнения) |
| `getComparisons` | GET | `/api/v1/comparisons?workspace_id={workspaceId}` | Список сравнений рабочей области |
| `deleteComparison` | DELETE | `/api/v1/comparisons/{id}` | Удалить сравнение |
### `documentations` — Сервис документации
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getDisks` | GET | `/api/v1/disks` | Список дисков |
| `getDocuments` | GET | `/api/v1/disks/{id}/documents` | Документы диска |
| `getNearestNameTemplate` | GET | `/api/v1/documents/{documentId}/name_template` | Ближайший шаблон имени документа |
### `sarexApi` — Gateway/API Sarex
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getDocumentsV2` | GET | `/gateway/api/v1/disks/{id}/documents?parent_id=&child_id=&search=&{attributeValue}` | Документы диска с фильтрами (родитель/ребёнок/поиск/атрибуты) |
### `sarex` — Локальный сервис данных
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getUsersByIDs` | GET | `/api/core/users/?id={ids}` | Пользователи по списку id |
### `workflows` — Сервис обработки документов
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getWorkflow` | GET | `/api/v1/workflows/{workflowId}` | Workflow по id |
### `bimv2` — BIM API v2
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getBim` | GET | `/api/v1/bims/{bimId}` | BIM-модель по id |
## Обработка ошибок
`fetch` перехватывает исключения запроса и возвращает `{ errMessage }` (строка ошибки) вместо данных; успешный ответ приходит как `{ resp: <data> }`. Явного маппинга кодов ответа в реестре нет — обработка и отображение ошибок выполняются на уровне репозиториев/вью-моделей, использующих `fetch`.

View File

@ -0,0 +1,793 @@
openapi: 3.0.3
info:
title: Comparisons Service API
version: "1.0.0"
description: |
REST API сервиса **comparisons-backend** (`pdm/comparisons-backend`) — создание и
просмотр сравнений (облако-BIM отклонение/статусы, облако-облако, облако-поверхность,
pdf-pdf), их элементов, изменений и фильтров.
Сервис написан на Go (роутер **gorilla/mux**). Сервер собирается функцией
`bootstrapAPI` в `cmd/api/bootstrap.go`. Роутинг состоит из двух групп:
- публичный API — префикс `/api/v1` (`cmd/api/routes_api.go`);
- внутренний API — префикс `/internal/v1` (`cmd/api/routes_internal.go`),
предназначен для вызовов внутри кластера (через ingress не публикуется).
### Аутентификация
Публичные эндпоинты (`/api/v1/*`) требуют JWT: middleware `auth.JWTToCtx` +
`auth.JWTUserExtractorFromCtx` (`gitlab.com/sarex-team/gotools/auth`). Токен
передаётся заголовком `Authorization: Bearer <jwt>`; middleware
`auth.DeleteJWTFromQueryMiddleware` дополнительно позволяет передать токен
query-параметром `jwt` (он удаляется из запроса после разбора).
Внутренние эндпоинты (`/internal/v1/*`) аутентификации на уровне приложения
не требуют — ограничение доступа обеспечивается сетевым слоем.
### Обработка ошибок
Ошибки возвращаются функцией `httperror.Write` (`gotools/httperror`). Основные
статусы: `400` — некорректный запрос/параметры, `404` — сравнение не найдено,
`500` — внутренняя ошибка. Идентификатор запроса пробрасывается middleware
`reqid`.
### Замечания (расхождения кода)
- `GET /api/v1/comparisons` возвращает данные разной формы в зависимости от
query-параметра: `workspace_id` → `{ comparisons: [] }`, `document_id` →
`{ results: [] }`, `bundle_id` → одиночный объект сравнения.
- `GET /api/v1/filter_fields` и `GET /api/v1/elements` работают только со
сравнениями типа `deviation`; для других типов вернётся `400`.
- Схема сравнения полиморфна по полю `type` (`deviation`/`c2c`/`c2s`/`abap`/`pdf2pdf`).
contact:
name: comparisons-backend
url: https://gitlab.com/sarex-team/comparisons-backend
servers:
- url: https://api.sarex.io/comparisons
description: Production (ingress)
- url: https://stage-api.sarex.io/comparisons
description: Stage (ingress)
- url: https://api.preprod.sarex.io/comparisons
description: Preprod (ingress)
- url: http://comparisons-backend-service.comparisons-stage
description: Внутрикластерный адрес (ClusterIP). Единственный способ достучаться до /internal/v1
- url: http://localhost:8080
description: Локальный запуск (API_ADDRESS по умолчанию 0.0.0.0:8080)
tags:
- name: types
description: Справочник типов сравнений
- name: comparisons
description: Сравнения — создание, просмотр, удаление
- name: elements
description: Элементы сравнения и их изменения
- name: internal
description: Внутренние эндпоинты (только внутри кластера)
security:
- bearerAuth: []
paths:
# ==========================================================================
# Types
# ==========================================================================
/api/v1/types:
get:
tags: [types]
summary: Справочник типов сравнений
description: Возвращает доступные типы сравнений и их параметры (для построения форм).
operationId: getTypes
responses:
'200':
description: Список типов сравнений
content:
application/json:
schema:
type: object
properties:
types:
type: array
items:
$ref: '#/components/schemas/CompareType'
# ==========================================================================
# Comparisons
# ==========================================================================
/api/v1/comparisons:
post:
tags: [comparisons]
summary: Создать сравнение
description: |
Создаёт сравнение указанного типа. Автор берётся из JWT. Если `parent_doc_id`
и `parent_disk_id` не заданы — вычисляются по рабочей области.
operationId: createComparison
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
'200':
description: Созданное сравнение
content:
application/json:
schema:
$ref: '#/components/schemas/Comparison'
'400':
description: Некорректный запрос / неизвестный тип сравнения
'500':
description: Внутренняя ошибка
get:
tags: [comparisons]
summary: Список сравнений
description: |
Возвращает сравнения по одному из query-параметров. Форма ответа зависит
от параметра (см. описание в `info`). Должен быть задан ровно один параметр.
operationId: getComparisons
parameters:
- name: workspace_id
in: query
required: false
schema:
type: string
format: uuid
description: "Сравнения рабочей области → ответ `{ comparisons: [] }`"
- name: bundle_id
in: query
required: false
schema:
type: string
format: uuid
description: Сравнение по бандлу → ответ — одиночный объект
- name: document_id
in: query
required: false
schema:
type: integer
format: int64
description: "Сравнения документа → ответ `{ results: [] }`"
responses:
'200':
description: Результат (форма зависит от параметра запроса)
content:
application/json:
schema:
oneOf:
- type: object
properties:
comparisons:
type: array
items:
$ref: '#/components/schemas/Comparison'
- type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/Comparison'
- $ref: '#/components/schemas/Comparison'
'400':
description: Не задан workspace_id / bundle_id / document_id
'500':
description: Внутренняя ошибка
/api/v1/comparisons/{comparison_id}:
parameters:
- name: comparison_id
in: path
required: true
schema:
type: integer
format: int64
get:
tags: [comparisons]
summary: Сравнение по id
operationId: getComparisonById
responses:
'200':
description: Сравнение
content:
application/json:
schema:
$ref: '#/components/schemas/Comparison'
'404':
description: Сравнение не найдено
'500':
description: Внутренняя ошибка
delete:
tags: [comparisons]
summary: Удалить сравнение
description: Мягкое удаление сравнения. Автор берётся из JWT.
operationId: deleteComparisonById
responses:
'200':
description: Успешно удалено
'400':
description: Некорректный id
'500':
description: Внутренняя ошибка
/api/v1/filter_fields:
get:
tags: [comparisons]
summary: Поля фильтрации сравнения
description: |
Возвращает набор полей фильтрации для сравнения типа `deviation` по документу.
Для полей отклонения (`deviation`, `deviation_x/y/z`) заполняются `min`/`max`.
Если сравнение по документу не найдено — вернётся пустой `results`.
operationId: getFilterFields
parameters:
- name: doc_id
in: query
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Поля фильтрации
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/FilterFields'
'400':
description: Не задан doc_id / сравнение не типа deviation
'500':
description: Внутренняя ошибка
/api/v1/tolerance:
get:
tags: [comparisons]
summary: Допуск (tolerance) сравнения
description: Возвращает значение допустимого отклонения для сравнения типа `deviation` по бандлу.
operationId: getTolerance
parameters:
- name: bundle_id
in: query
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Допуск
content:
application/json:
schema:
type: object
properties:
tolerance:
type: number
format: double
'400':
description: Не задан / некорректный bundle_id
'404':
description: Сравнение не найдено
'500':
description: Внутренняя ошибка
# ==========================================================================
# Elements
# ==========================================================================
/api/v1/elements:
get:
tags: [elements]
summary: Список элементов сравнения
description: |
Постранично возвращает элементы сравнения типа `deviation` по документу.
Параметры отклонения задаются строкой вида `min:max`. Если сравнение не
найдено — вернётся пустой `results` с типами по умолчанию.
operationId: getListElements
parameters:
- name: doc_id
in: query
required: true
schema:
type: integer
format: int64
- name: limit
in: query
required: false
schema:
type: integer
default: 10
minimum: 0
- name: offset
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
- name: order_by
in: query
required: false
schema:
type: string
enum: [deviation, deviation_x, deviation_y, deviation_z, sarex_id, name]
default: sarex_id
- name: order
in: query
required: false
schema:
type: string
enum: [asc, desc]
default: asc
- name: deviation
in: query
required: false
schema:
type: string
description: Диапазон общего отклонения в формате `min:max`
- name: deviation_x
in: query
required: false
schema:
type: string
description: Диапазон отклонения по X в формате `min:max`
- name: deviation_y
in: query
required: false
schema:
type: string
description: Диапазон отклонения по Y в формате `min:max`
- name: deviation_z
in: query
required: false
schema:
type: string
description: Диапазон отклонения по Z в формате `min:max`
- name: deviation_status
in: query
required: false
schema:
type: string
enum: [in_tolerance, out_of_tolerance]
- name: view_status
in: query
required: false
schema:
type: string
enum: [viewed, not_viewed]
- name: only_with_comment
in: query
required: false
schema:
type: boolean
responses:
'200':
description: Страница элементов
content:
application/json:
schema:
$ref: '#/components/schemas/ListElementsResponse'
'400':
description: Некорректные параметры запроса
'500':
description: Внутренняя ошибка
/api/v1/elements/{element_id}:
parameters:
- name: element_id
in: path
required: true
schema:
type: integer
format: int64
patch:
tags: [elements]
summary: Обновить элемент
description: |
Обновляет один из атрибутов элемента: комментарий, статус отклонения или
статус просмотра. Должно быть задано ровно одно из полей. Изменение
фиксируется записью в истории изменений.
operationId: updateElement
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateElementRequest'
responses:
'200':
description: Успешно обновлено (тело не возвращается)
'400':
description: Некорректный запрос
'500':
description: Внутренняя ошибка
/api/v1/elements/{element_id}/changes:
get:
tags: [elements]
summary: История изменений элемента
operationId: getElementChanges
parameters:
- name: element_id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Список изменений
content:
application/json:
schema:
type: object
properties:
changes:
type: array
items:
$ref: '#/components/schemas/Change'
'400':
description: Некорректный element_id
'500':
description: Внутренняя ошибка
# ==========================================================================
# Internal
# ==========================================================================
/internal/v1/comparisons/{comparison_id}/webhook:
post:
tags: [internal]
summary: Webhook результата сравнения (внутренний)
description: |
Вызывается после завершения обработки сравнения типа `deviation`. Скачивает
`deviation_json` из PDM-хранилища по бандлу, создаёт элементы сравнения и
возвращает содержимое `deviation_json`. Аутентификация на уровне приложения
не требуется.
operationId: comparisonWebhook
security: []
parameters:
- name: comparison_id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Содержимое deviation_json
content:
application/json:
schema:
$ref: '#/components/schemas/DeviationJSON'
'404':
description: Сравнение не найдено
'500':
description: Внутренняя ошибка
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT в заголовке `Authorization: Bearer <jwt>`. Допускается передача
query-параметром `jwt` (удаляется middleware после разбора).
schemas:
ComparisonType:
type: string
enum: [c2c, c2s, deviation, abap, pdf2pdf]
CompareType:
type: object
description: Тип сравнения и набор его параметров (для формы создания).
properties:
name:
$ref: '#/components/schemas/ComparisonType'
verbose_name:
type: string
params:
type: array
items:
$ref: '#/components/schemas/CompareFormParam'
CompareFormParam:
type: object
properties:
name:
type: string
verbose_name:
type: string
hint:
type: string
source:
type: string
enum: [form, viewer]
type:
type: string
description: Тип поля (напр. cloud, bimv2, number, multichoice, choice, array, pdf, surface)
options:
type: array
items:
type: object
additionalProperties: true
default: {}
min:
type: number
format: double
max:
type: number
format: double
CreateRequest:
type: object
required: [name, workspace_id, type, company_id, params]
properties:
name:
type: string
workspace_id:
type: string
format: uuid
parent_doc_id:
type: integer
format: int64
nullable: true
parent_disk_id:
type: string
format: uuid
nullable: true
type:
$ref: '#/components/schemas/ComparisonType'
company_id:
type: integer
format: int64
params:
type: object
description: Параметры сравнения, зависят от type (см. /api/v1/types).
additionalProperties: true
Comparison:
type: object
description: |
Полиморфное сравнение. Поле `type` определяет набор `params`. Ниже приведён
пример для типа `deviation`; для других типов набор params отличается.
properties:
id:
type: integer
format: int64
type:
$ref: '#/components/schemas/ComparisonType'
created_by:
type: integer
format: int64
name:
type: string
workspace_id:
type: string
format: uuid
workflow_id:
type: string
format: uuid
nullable: true
document_id:
type: integer
format: int64
nullable: true
bundle_id:
type: string
format: uuid
nullable: true
params:
$ref: '#/components/schemas/DeviationParams'
DeviationParams:
type: object
properties:
cloud_bundle_id:
type: string
format: uuid
model_bundle_id:
type: string
format: uuid
down_sample_size:
type: number
format: double
resolution:
type: number
format: double
linear_margin:
type: number
format: double
tolerance:
type: number
format: double
sarex_ids:
type: array
items:
type: integer
format: int64
transformation:
type: array
items:
type: number
format: double
applied_statuses:
type: array
items:
type: string
crop_margin:
type: number
format: double
nullable: true
Element:
type: object
properties:
id:
type: integer
format: int64
sarex_id:
type: integer
format: int64
name:
type: string
deviation_status:
type: string
enum: [in_tolerance, out_of_tolerance]
view_status:
type: string
enum: [viewed, not_viewed]
deviation:
type: number
format: double
deviation_x:
type: number
format: double
deviation_y:
type: number
format: double
deviation_z:
type: number
format: double
axis_and_angle:
type: object
description: Ось и угол поворота (utils.AxisAndAngle)
additionalProperties: true
comment:
type: string
nullable: true
bbox_matrix:
type: array
items:
type: number
format: double
reg_matrix:
type: array
items:
type: number
format: double
ListElementsResponse:
type: object
properties:
count:
type: integer
next:
type: string
nullable: true
previous:
type: string
nullable: true
types:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/Option'
crop_margin:
type: number
format: double
nullable: true
results:
type: array
items:
$ref: '#/components/schemas/Element'
UpdateElementRequest:
type: object
description: |
Должно быть задано ровно одно из полей (`comment`, `deviation_status`,
`view_status`) — валидатор `required_without_all`.
properties:
comment:
type: string
deviation_status:
type: string
enum: [in_tolerance, out_of_tolerance]
view_status:
type: string
enum: [viewed, not_viewed]
Change:
type: object
properties:
id:
type: integer
format: int64
element_id:
type: integer
format: int64
created_at:
type: string
format: date-time
author:
type: integer
format: int64
changed_field:
type: string
enum: [deviation_status, view_status, comment]
old_value:
type: string
new_value:
type: string
FilterFields:
type: object
properties:
name:
type: string
enum: [deviation, deviation_x, deviation_y, deviation_z, deviation_status, view_status, only_with_comment]
verbose_name:
type: string
type:
type: string
enum: [checkbox, selector, slider]
options:
type: array
items:
$ref: '#/components/schemas/Option'
min:
type: number
format: double
nullable: true
max:
type: number
format: double
nullable: true
Option:
type: object
properties:
name:
type: string
verbose_name:
type: string
DeviationJSON:
type: object
properties:
crop_margin:
type: number
format: double
nodes:
type: object
additionalProperties:
$ref: '#/components/schemas/Node'
Node:
type: object
properties:
bbox_matrix:
type: array
items:
type: number
format: double
reg_matrix:
type: array
items:
type: number
format: double
node_name:
type: string

View File

@ -0,0 +1,19 @@
# App
LOG_LEVEL=debug
ADDRESS=:8080
# Auth
# Публичный RSA-ключ (PEM) для проверки JWT.
# Читается напрямую через os.Getenv("PUBLIC_KEY") в cmd/http/main.go.
PUBLIC_KEY=
# Database
# DSN подключения к PostgreSQL (pgx), напр. postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable
DB_URL=postgres://postgres:admin@127.0.0.1:5432/postgres?sslmode=disable
DB_POOL_SIZE=10
# CLI (миграции) — cmd/cli
# Путь к каталогу с миграциями (по умолчанию migrations)
DB_MIGRATIONS_PATH=migrations
# Необязательно: путь к .env-файлу для cli (эквивалент флага -env-file)
# ENV_FILE=.env

View File

@ -0,0 +1,135 @@
# Конфигурация проекта contracts
Документ описывает все переменные окружения и способы конфигурирования сервиса `contracts` (Go, HTTP API + CLI миграций).
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется библиотекой [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig) по структуре `Config` в `internal/app/http/config.go`. Дополнительно `.env`-файл автоматически подгружается через [`github.com/joho/godotenv`](https://github.com/joho/godotenv):
- HTTP-процесс (`cmd/http/main.go`) вызывает `godotenv.Load(".env")` перед разбором конфигурации — если файл `.env` есть в рабочем каталоге, его переменные попадают в окружение;
- CLI-процесс (`cmd/cli/main.go`) загружает файл из `ENV_FILE` (или `.env` по умолчанию), путь можно задать флагом `-env-file`.
Особенности разбора (`go-envconfig`):
- глобального префикса нет — верхнеуровневые поля читаются по своим именам (`LOG_LEVEL`, `ADDRESS`);
- вложенные секции задаются префиксом на уровне структуры: `Database``env:", prefix=DB_"`, `Auth``env:", prefix=AUTH_"`;
- значения по умолчанию заданы в тегах через `default=…`; поля без `default` при отсутствии переменной остаются пустыми (нулевым значением типа), а не приводят к панике на этапе разбора — ошибки всплывают позже (например, невалидный `DB_URL` или пустой `PUBLIC_KEY`).
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | `.env` в рабочем каталоге (авто-загрузка `godotenv`) + переменные окружения процесса |
| Локально (docker-compose) | `docker-compose.yml`: сервис `contracts` берёт переменные из `env_file: .env`; поднимается вместе с `postgres` |
| Kubernetes (Helm) | `.helm/values-<env>.yaml`: блоки `envs` (обычные значения) и `secrets` (значения из k8s-секретов); шаблон `.helm/templates/deployment.yaml` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` и переменные `workflow.rules` (namespace, release, chart) |
Способы запуска процессов:
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `http` | `cmd/http/main.go` | HTTP API (Fiber v3), слушает `ADDRESS` |
| `cli migrate` | `cmd/cli/main.go` | Применение миграций БД (`golang-migrate`), каталог `DB_MIGRATIONS_PATH` |
Порядок запуска в контейнере (`entrypoint.sh`): сначала `./cli migrate`, затем `./http`.
## Переменные приложения
В столбце «Переменная» указано полное имя (с учётом префикса секции). Дефолт `—` означает, что значения по умолчанию нет.
### App
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `debug` | Уровень логирования (zap): `debug`/`info`/`warn`/`error` и т.п. |
| `ADDRESS` | string | `:8080` | Адрес и порт прослушивания HTTP-сервера (Fiber) |
### Database (`DB_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DB_URL` | string | — | DSN подключения к PostgreSQL (`pgxpool.ParseConfig`), напр. `postgres://user:pass@host:5432/db?sslmode=verify-full` |
| `DB_POOL_SIZE` | int32 | `10` | Максимальный размер пула соединений (`pgxpool.Config.MaxConns`) |
### Auth (`AUTH_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AUTH_PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM) для проверки JWT. См. замечание ниже — фактически используется `PUBLIC_KEY` |
| `PUBLIC_KEY` | string | — | Публичный RSA-ключ (PEM). Читается напрямую в `cmd/http/main.go` через `os.Getenv("PUBLIC_KEY")` и записывается в `config.Auth.PublicKey`, перекрывая `AUTH_PUBLIC_KEY` |
> При старте `AuthProvider` парсит ключ (`pem.Decode` + `x509.ParsePKIXPublicKey`). Если `PUBLIC_KEY` пустой или невалидный — приложение падает с `panic` ещё до старта HTTP-сервера.
## Переменные CLI (миграции)
Читаются в `cmd/cli/main.go` (структура `cliConfig`, префикс `DB_`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DB_URL` | string | — | DSN подключения к PostgreSQL для применения миграций (обязателен, иначе ошибка `DB_URL is required`) |
| `DB_MIGRATIONS_PATH` | string | `migrations` | Путь к каталогу с SQL-миграциями (`golang-migrate`) |
| `ENV_FILE` | string | `.env` | Путь к `.env`-файлу, из которого CLI загружает переменные (можно задать флагом `-env-file=PATH`) |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная / параметр | Где используется | Назначение |
| --- | --- | --- |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | `docker-compose.yml` | Параметры локального контейнера PostgreSQL (`postgres`/`admin`/`postgres`) |
| build-stage `golang:1.24` | `Dockerfile` | Базовый образ для сборки бинарников `http` и `cli` |
| runtime `alpine:latest` | `Dockerfile` | Финальный образ; копируются `http`, `cli`, `migrations/`, `entrypoint.sh`; открыт порт `8080` |
## Переменные из Helm-чарта (`.helm/values-<env>.yaml`)
Обычные значения задаются в блоке `envs` (в текущих values он пуст: `envs: []`). Значения из секретов (блок `secrets`) монтируются как env через `secretKeyRef` в `.helm/templates/deployment.yaml`:
| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) |
| --- | --- | --- |
| `DB_URL` | `ya-pg-secret` | `db_url` |
| `PUBLIC_KEY` | `public-key` | `key` |
Прочие значения чарта (не переменные приложения): `deployment.*` (имя, образ, порт, реплики, ресурсы, `service_name`/`service_port`), `api.*` (host/prefix/path ingress), `imagePullSecrets`.
Помимо env, чарт монтирует CA-сертификат PostgreSQL из секрета `ya-pg-secret` (ключ `certificate`) как файл `/opt/.postgresql/root.crt` (см. `deployment.yaml`). Секрет `ya-pg-secret` при отсутствии создаётся шаблоном `ya-pg-secret.yaml` со случайными значениями и политикой `helm.sh/resource-policy: keep`.
Параметры окружений (`.helm/values-<env>.yaml`):
| Окружение | `api.host` | `deployment.service_port` |
| --- | --- | --- |
| stage | `stage-api.sarex.io` | `8080` |
| preprod | `api.preprod.sarex.io` | `80` |
| production | `api.sarex.io` | `8080` |
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | Chart version |
| --- | --- | --- | --- |
| ветка `master` | `preprod` | `contracts-preprod` | `0.0.1-preprod` |
| ветка `stage` | `stage` | `contracts-stage` | `0.0.1-stage` |
| тег (`CI_COMMIT_TAG`) | `prod` | `contracts-prod` | `0.0.1-prod` |
Общие переменные пайплайна: `RELEASE_NAME=contracts`, `CHART_NAME=contracts`, `IMAGE_PATH=deployment.image`, `HELM_SET_ARGS="--set deployment.image=${IMAGE_NAME}"`, `DOCKERFILE_PATH=Dockerfile`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_STATE_UPDATE`/`ENABLE_DEPLOY` (`true`), `ENABLE_LINTER` (`false`). Для merge request-ов пайплайн запускается без деплоя.
## Замечания и потенциальные проблемы
- **Дублирование ключа авторизации.** В `Config` объявлено поле `Auth.PublicKey` с тегом `AUTH_PUBLIC_KEY`, но `cmd/http/main.go` дополнительно читает `os.Getenv("PUBLIC_KEY")` и перезаписывает им значение. В Helm секрет прокидывается как `PUBLIC_KEY`. Практически используется именно `PUBLIC_KEY`; `AUTH_PUBLIC_KEY` в текущем деплое не задаётся.
- **`.env` загружается автоматически** (в отличие от Python-сервисов): `godotenv.Load(".env")` в HTTP-процессе и `godotenv.Load(ENV_FILE|.env)` в CLI. Файл `.env` при этом попадает под `.gitignore` (`*.env`) и в репозиторий не коммитится.
- **Пустой `PUBLIC_KEY` — фатально.** `auth.New` делает `panic`, если ключ не удаётся распарсить как PEM/PKIX. Для локального запуска нужен валидный публичный ключ.
- **`DB_URL` обязателен и для http, и для cli.** Невалидный DSN приводит к ошибке `pgxpool.ParseConfig`/подключения; в CLI пустой `DB_URL` даёт явную ошибку `DB_URL is required`.
- **`envs: []` в values.** Все прикладные переменные в k8s сейчас приходят только из секретов (`DB_URL`, `PUBLIC_KEY`); `LOG_LEVEL`/`ADDRESS` используют дефолты (`debug`, `:8080`).
## Минимальный набор для локального запуска
PostgreSQL поднимается через `docker-compose up postgres`, приложение — сборкой `cmd/http` (или целиком через docker-compose). Минимально необходимо задать:
- `DB_URL` — DSN до PostgreSQL (для локали обычно `?sslmode=disable`)
- `PUBLIC_KEY` — валидный публичный RSA-ключ (PEM) для проверки JWT
- при необходимости: `LOG_LEVEL`, `ADDRESS`, `DB_POOL_SIZE` (иначе применяются дефолты)
- для миграций (`cli migrate`): `DB_URL` и, при нестандартном расположении, `DB_MIGRATIONS_PATH`
Готовые значения-примеры приведены в `.env.example`.

View File

@ -0,0 +1,64 @@
# Эндпоинты, с которыми взаимодействует contracts-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `contracts-frontend`).
## Как устроено взаимодействие
Запросы сгруппированы по доменам в каталоге `src/shared/api/fetch/*.api.ts`. Каждая функция вызывает соответствующий метод `httpService` (`src/shared/api/http-service.ts`), который создаётся фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). Для запроса указываются:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (добавляется к базовому хосту сервиса);
- `data` — тело запроса (для `post`/`put`);
- `axiosConfig.params` — query-параметры;
- `cache`, `queryKey` — опции кеширования (react-query-подобные ключи из `src/shared/api/keys/*`);
- `isCSRF` — включение CSRF-обработки (для `departments`).
Базовый хост подставляется по значению `service` и текущему окружению `__BUILD_ENV__` (`local`/`stage`/`preprod`/`prod`, по умолчанию `prod`; см. `http-service.ts`). В режиме `local` для http-сервиса устанавливается тип `zitadel` (`setTypeOfHttpService("zitadel")`). Итоговый URL = `<базовый хост сервиса>` + `url`.
## Базовые хосты по сервисам и окружениям
Значения из `src/shared/api/hosts.ts`. Ниже перечислены сервисы, **фактически используемые** запросами модуля; в реестре хостов определены и другие сервисы (`bim`, `bimv2`, `workflows`, `workspaces`, `documentations`, `comparisons`, `remarks`, `projects`, `eavV1`, `notifications`, `google`, `sarexApi`, `zitadel`), но обращений к ним в `fetch/*` нет.
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `contracts` | Сервис договоров (contracts-backend) | `https://stage-api.sarex.io/contracts` | `https://api.sarex.io/contracts` |
| `sarex` | Локальный backend Sarex (core/admin) | `""` (относительные пути) | `""` |
| `gateway` | Gateway/API Sarex (ресурсы/проекты) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
> Также определены окружения `local` и `preprod`. В `local` сервисы проксируются на относительные пути (`contracts` → `/sarex-contracts`, `sarex``/sarex-backend`, `gateway``/sarex-gateway`, `zitadel``/zitadel`). Значения `preprod` используют домен `api.preprod.sarex.io`.
## Эндпоинты по сервисам
### `contracts` — Сервис договоров
Определены в `src/shared/api/fetch/contract.api.ts`.
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchContractsByResourceId` | GET | `/api/v0/contracts` | Список договоров (query: `limit`, `offset`, `resource_id`, `tenant_id`) |
| `fetchCreateContractByResourceId` | POST | `/api/v0/contracts` | Создать договор |
| `fetchUpdateContract` | PUT | `/api/v0/contracts/{contract.id}` | Обновить договор по id |
> Функция удаления `fetchDeleteContract` (`DELETE /api/v0/contracts/{contractId}`) присутствует в коде, но закомментирована.
### `sarex` — Локальный backend Sarex (core/admin)
Определены в `company.api.ts`, `contractor.api.ts`, `department.api.ts`.
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchCompanies` | GET | `/api/core/admin/companies/` | Список компаний (кешируется, ключ `companies`) |
| `fetchContractors` | GET | `/api/core/admin/contractors/?company_id={companyId}` | Контрагенты компании (кешируется, ключ `contractors`) |
| `fetchDepartments` | GET | `/api/core/admin/departments/` | Отделы (кешируется, ключ `departments`, `isCSRF: true`) |
### `gateway` — Gateway/API Sarex
Определён в `project.api.ts`.
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchProjects` | GET | `/api/v1/resources/?company_id={companyId}` | Список ресурсов/проектов компании (кешируется, ключ `projects`) |
## Обработка запросов и кеширование
Кеширование включается флагом `cache: true` с ключом `queryKey` (значения ключей — в `src/shared/api/keys/*.ts`: `companies`, `projects`, `contractors`, `departments`). Обработка ошибок и авторизация (в т.ч. режим `zitadel` для `local`) выполняются внутри `httpService` из `@sarex-team/sdk-js`.

404
apps/contracts/openapi.yaml Normal file
View File

@ -0,0 +1,404 @@
openapi: 3.0.3
info:
title: Contracts Service API
version: "1.0.0"
description: |
REST API сервиса **contracts** (`platform/contracts`) — управление
договорами (контрактами): создание (в т.ч. массовое), получение по id,
списочный вывод с фильтрами и пагинацией, обновление.
Сервис написан на Go (**Fiber v3**). Приложение собирается в
`internal/app/http/app.go` (`New`). Роутинг вложен под общий префикс
`/api/v0` (`app.server.Group("/api/v0")`), внутри — группа
`/contracts` (`internal/controller/http/v0`).
### Аутентификация
Все эндпоинты группы `/api/v0/contracts` защищены middleware
(`internal/adapter/auth/middleware.go`). Поддерживаются два режима:
1. **Zitadel** — если передан заголовок `Identity: Bearer <jwt>`,
пользователь берётся из полезной нагрузки этого токена
(`urn:zitadel:iam:user:metadata`). Подпись на уровне приложения
не проверяется (валидность обеспечивается сетевым слоем/Istio).
2. **sarex-backend** — если заголовка `Identity` нет, подпись основного
токена `Authorization: Bearer <jwt>` проверяется публичным RSA-ключом
(`PUBLIC_KEY`).
Корневой эндпоинт `GET /api/v0/` (health/ping) аутентификации не требует.
### Идентификаторы
Идентификатор договора — **ULID** (строка), парсится через
`ulid.Parse`. `resource_id` — **UUID**.
### Пагинация
Списочный вывод использует `limit`/`offset` (query-параметры, по умолчанию
`limit=100`, `offset=0`).
### Обработка ошибок
Ошибки возвращаются с соответствующим HTTP-статусом; тело — либо строка
с описанием, либо `{"error": "..."}` (при внутренней панике). Коды:
`400` — некорректный запрос/невалидный id, `401` — проблемы аутентификации,
`403` — пользователь не состоит в компании (`tenant_id`), `404` — договор
не найден, `500` — внутренняя ошибка, `501` — метод не реализован.
### Замечания (расхождения кода)
- `PATCH` и `DELETE` (как по коллекции, так и по id) возвращают
**`501 Not Implemented`** — обработчики-заглушки.
- `POST /api/v0/contracts` принимает **как одиночный объект, так и массив**:
тип создания выбирается по форме тела (объект → создание одного договора,
массив → пакетное создание).
- `GET /api/v0/contracts` (список) требует query-параметр `tenant_id`
(middleware `UserInCompanyMiddleware` проверяет, что пользователь состоит
в этой компании; иначе `400`/`403`).
servers:
- url: https://api.sarex.io/contracts
description: production
- url: https://stage-api.sarex.io/contracts
description: stage
- url: https://api.preprod.sarex.io/contracts
description: preprod
security:
- bearerAuth: []
tags:
- name: contracts
description: Договоры
- name: service
description: Служебные эндпоинты
paths:
/api/v0/:
get:
tags: [service]
summary: Health / ping
description: Возвращает `200 OK` без тела. Аутентификация не требуется.
security: []
responses:
"200":
description: OK
/api/v0/contracts:
post:
tags: [contracts]
summary: Создать договор или несколько договоров
description: |
Принимает либо одиночный объект `CreateContractRequest`, либо массив
таких объектов. Форма тела определяет режим создания.
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/CreateContractRequest"
- type: array
items:
$ref: "#/components/schemas/CreateContractRequest"
responses:
"201":
description: Договор(ы) создан(ы)
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/ContractResponse"
- type: array
items:
$ref: "#/components/schemas/ContractResponse"
"400":
description: Некорректное тело запроса / ошибка валидации
"401":
description: Ошибка аутентификации
"500":
description: Внутренняя ошибка
get:
tags: [contracts]
summary: Список договоров
description: |
Возвращает договоры с фильтрацией и пагинацией. Требует `tenant_id`
(проверяется принадлежность пользователя к компании).
parameters:
- name: tenant_id
in: query
required: true
schema:
type: integer
format: int64
description: Идентификатор компании/арендатора
- name: limit
in: query
required: false
schema:
type: integer
format: int64
default: 100
- name: offset
in: query
required: false
schema:
type: integer
format: int64
default: 0
- name: resource_id
in: query
required: false
schema:
type: string
format: uuid
- name: contractor_id
in: query
required: false
schema:
type: integer
format: int64
responses:
"200":
description: Список договоров
content:
application/json:
schema:
$ref: "#/components/schemas/ContractPaginatedResponse"
"400":
description: Некорректные параметры запроса / отсутствует tenant_id
"401":
description: Ошибка аутентификации
"403":
description: Пользователь не состоит в указанной компании
"500":
description: Внутренняя ошибка
put:
tags: [contracts]
summary: Пакетное обновление договоров
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/UpdateContractRequest"
responses:
"200":
description: Договоры обновлены
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ContractResponse"
"400":
description: Некорректное тело запроса
"401":
description: Ошибка аутентификации
"500":
description: Внутренняя ошибка
patch:
tags: [contracts]
summary: Пакетное частичное обновление (не реализовано)
responses:
"501":
description: Not Implemented
delete:
tags: [contracts]
summary: Пакетное удаление (не реализовано)
responses:
"501":
description: Not Implemented
/api/v0/contracts/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Идентификатор договора (ULID)
get:
tags: [contracts]
summary: Получить договор по id
responses:
"200":
description: Договор
content:
application/json:
schema:
$ref: "#/components/schemas/ContractResponse"
"400":
description: Невалидный id
"401":
description: Ошибка аутентификации
"404":
description: Договор не найден
"500":
description: Внутренняя ошибка
put:
tags: [contracts]
summary: Обновить договор
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateContractRequest"
responses:
"200":
description: Договор обновлён
content:
application/json:
schema:
$ref: "#/components/schemas/ContractResponse"
"400":
description: Невалидный id / некорректное тело
"401":
description: Ошибка аутентификации
"404":
description: Договор не найден
"500":
description: Внутренняя ошибка
patch:
tags: [contracts]
summary: Частичное обновление (не реализовано)
responses:
"501":
description: Not Implemented
delete:
tags: [contracts]
summary: Удалить договор (не реализовано)
responses:
"501":
description: Not Implemented
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
Основной токен `Authorization: Bearer <jwt>` (проверяется по RSA-ключу).
Опционально может передаваться заголовок `Identity: Bearer <jwt>`
(режим Zitadel), который имеет приоритет.
schemas:
Contractor:
type: object
description: Произвольный JSON-объект с данными контрагента (в БД — JSONB).
additionalProperties: true
CreateContractRequest:
type: object
required: [number, tenant_id, started_at, deadline_at]
properties:
number:
type: string
minLength: 1
description: Номер договора
tenant_id:
type: integer
format: int64
description: Идентификатор компании/арендатора
resource_id:
type: string
format: uuid
nullable: true
contractor:
$ref: "#/components/schemas/Contractor"
started_at:
type: string
format: date-time
deadline_at:
type: string
format: date-time
cost:
type: number
format: double
minimum: 0
description:
type: string
UpdateContractRequest:
type: object
required: [number, tenant_id, started_at, deadline_at]
properties:
id:
type: string
description: ULID договора
number:
type: string
minLength: 1
tenant_id:
type: integer
format: int64
resource_id:
type: string
format: uuid
nullable: true
contractor:
$ref: "#/components/schemas/Contractor"
started_at:
type: string
format: date-time
deadline_at:
type: string
format: date-time
cost:
type: number
format: double
minimum: 0
description:
type: string
ContractResponse:
type: object
properties:
id:
type: string
description: ULID договора
number:
type: string
tenant_id:
type: integer
format: int64
resource_id:
type: string
format: uuid
nullable: true
contractor:
$ref: "#/components/schemas/Contractor"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
started_at:
type: string
format: date-time
deadline_at:
type: string
format: date-time
cost:
type: number
format: double
description:
type: string
ContractPaginatedResponse:
type: object
properties:
count:
type: integer
format: int64
limit:
type: integer
format: int64
offset:
type: integer
format: int64
results:
type: array
items:
$ref: "#/components/schemas/ContractResponse"

View File

@ -0,0 +1,162 @@
# Эндпоинты, с которыми взаимодействует srx-admin
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается приложение `srx-admin` (панель администрирования, деплой `control-interface`).
## Как устроено взаимодействие
`srx-admin` — это монорепозиторий (`admin-monorepo`) c двумя фронтенд-сервисами и общим пакетом:
- `services/admin` — хост-приложение (основной админ-интерфейс);
- `services/assets` — федеративный модуль (Module Federation), встраиваемый в хост;
- `packages/app-kit` — общий пакет с реестром API-функций и таблицей хостов.
Запросы описаны не единым реестром, а по доменам — в файлах `shared/api/fetch/*.api.ts`. Каждый домен экспортирует фабрику (например `UserApi`, `AssetApi`, `ProjectApi`), которая принимает `httpService` и возвращает набор методов. Внутри метода вызывается `httpService.getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest` со структурой:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (с подстановкой параметров прямо в строку или через `axiosConfig.params`);
- `data` — тело запроса (для POST/PUT/PATCH);
- `axiosConfig`, `cache`, `queryKey`, `controller`, `isCSRF` — опции axios, кеширования, ключа запроса, отмены и CSRF-токена.
`httpService` создаётся в `shared/api/http-service.ts` через `createHttpService` из `@sarex-team/sdk-js`. Базовый хост подставляется по логическому имени `service` из `packages/app-kit/src/shared/api/hosts.ts` в зависимости от окружения сборки `__ENDPOINT__` (`BUILD_ENV`, по умолчанию `prod`). Итоговый URL = `<базовый хост сервиса>` + `url`.
## Базовые хосты по сервисам и окружениям
Значения из `packages/app-kit/src/shared/api/hosts.ts`. Ниже приведены `stage` и `prod`; дополнительно определены окружения `local`, `contour` и `preprod` (см. примечание). Сервисы, к которым `srx-admin` реально обращается, отмечены значком «●» в колонке «Используется».
| Сервис (`service`) | Назначение | Используется | `stage` | `prod` |
| --- | --- | --- | --- | --- |
| `iam` | IAM: пользователи, отделы, должности, группы, права | ● | `https://stage-api.sarex.io/iam` | `https://api.sarex.io/iam` |
| `eavV1` | EAV: ассеты, атрибуты, права на ассеты, модули | ● | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` |
| `gateway` | Gateway: ресурсы (проекты) и права на ресурсы | ● | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/pm`, `/api/commons`) | ● | `""` (относительные пути) | `""` |
| `bimv2` | BIM v2: модели статусов | ● | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` |
| `premises` | Сервис помещений | ● | `https://stage-api.sarex.io/premises` | `https://api.sarex.io/premises` |
| `notifications` | Лямбда уведомлений (email) | ● | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` |
| `documentations` | Сервис документации | | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` |
| `workspaces` | Сервис рабочих областей | | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` |
| `workflows` | Сервис обработки документов | | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` |
| `comparisons` | Сервис сравнений | | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` |
| `remarks` | Сервис замечаний | | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` |
| `projects` | Сервис проектов | | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` |
| `bim` | BIM-API | | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` |
| `sarexApi` | Gateway/API Sarex (корень) | | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `google` | Временное хранилище (GCS) | | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` |
| `zitadel` | IdP (аутентификация) | | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> В окружении `local` сервисы проксируются на относительные пути (`sarex` → `/sarex-backend`, `gateway``/sarex-gateway`, `eavV1``/sarex-eav-v1`, `notifications``/sarex-notifications`, `iam``/iam`, `premises``/premises` и т. д.). Окружение `contour` использует относительные пути для изолированного контура. Подключаемые удалённые модули (Module Federation) описаны отдельно в `services/*/config/endpoints.ts` (см. раздел «Удалённые модули»).
## Эндпоинты по сервисам
### `iam` — IAM (пользователи, отделы, должности, группы, права)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchUsers` | GET | `/api/admin/v0/users/?{query}` | Список пользователей компании (пагинация, поиск, фильтры) |
| `fetchCreateUser` | POST | `/api/admin/v0/users/` | Создать пользователя |
| `fetchUpdateUser` | PATCH | `/api/admin/v0/users/{id}/` | Обновить пользователя |
| `fetchBulkUpdateUsers` | PATCH | `/api/admin/v0/users/` | Массовое обновление пользователей |
| `fetchBulkUpdateUsersActivation` | POST | `/api/admin/v0/users/activation/` | Массовая активация/деактивация пользователей |
| `fetchDepartments` | GET | `/api/admin/v0/departments/` | Список отделов (пагинация, поиск, фильтр по компании) |
| `fetchCreateDepartment` | POST | `/api/admin/v0/departments/` | Создать отдел (CSRF) |
| `fetchUpdateDepartment` | PUT | `/api/admin/v0/departments/{id}/` | Обновить отдел (CSRF) |
| `fetchDeleteDepartment` | DELETE | `/api/admin/v0/departments/{id}/` | Удалить отдел (CSRF) |
| `fetchPositions` | GET | `/api/admin/v0/positions` | Список должностей (пагинация, поиск, фильтр по компании) |
| `fetchCreatePosition` | POST | `/api/admin/v0/positions/` | Создать должность (CSRF) |
| `fetchUpdatePosition` | PUT | `/api/admin/v0/positions/{id}/` | Обновить должность (CSRF) |
| `fetchDeletePosition` | DELETE | `/api/admin/v0/positions/{id}/` | Удалить должность (CSRF) |
| `fetchGroups` | POST | `/api/admin/v0/groups/search/` | Поиск функциональных групп (фильтры, пагинация, сортировка) |
| `createGroup` | POST | `/api/admin/v0/groups` | Создать группу |
| `updateGroup` | PATCH | `/api/admin/v0/groups/{id}` | Обновить группу |
| `deleteGroup` | DELETE | `/api/admin/v0/groups/{id}` | Удалить группу |
| `fetchPermissions` | POST | `/api/admin/v0/permissions/search/` | Поиск прав (фильтры, пагинация, сортировка) |
| `createPermission` | POST | `/api/admin/v0/permissions` | Создать право |
| `deletePermission` | DELETE | `/api/admin/v0/permissions/{id}` | Удалить право |
### `eavV1` — EAV (ассеты, атрибуты, права на ассеты, модули)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchGetAssetsV4` | GET | `/api/v4/assets/` | Список ассетов (v4, параметры фильтрации) |
| `fetchGetAssetsV5` | GET | `/api/v2/assets/` | Список ассетов (v5) |
| `fetchCreateBulkAssetsV4` | POST | `/api/v4/assets/` | Массовое создание ассетов (v4) |
| `fetchCreateBulkAssetsV5` | POST | `/api/v2/assets/` | Массовое создание ассетов (v5) |
| `fetchUpdateBulkAssetsV4` | PATCH | `/api/v4/assets/` | Массовое обновление ассетов (v4) |
| `fetchUpdateBulkAssetsV5` | PATCH | `/api/v2/assets/` | Массовое обновление ассетов (v5) |
| `fetchDeleteAssetV4` | DELETE | `/api/v4/assets/{assetId}/` | Удалить ассет (v4) |
| `fetchDeleteAssetV5` | DELETE | `/api/v2/assets/{assetId}/` | Удалить ассет (v5) |
| `fetchCopyRootAsset` | POST | `/api/v4/assets/{asset_id}/copy/` | Копировать корневой ассет |
| `fetchCopyAssets` | POST | `/api/v2/assets/copy-to-destination-bulk/` | Массовое копирование ассетов в назначения |
| `fecthGetAssetPermissions` | GET | `/api/v4/permissions/?asset_id={id}&service_account_id={id}` | Права доступа ассета |
| `fetchPostCreateAssetPermissions` | POST | `/api/v4/permissions/` | Создать права на ассет |
| `fetchPostUpdateAssetPermissions` | PATCH | `/api/v4/permissions/` | Обновить права на ассет |
| `fetchDeleteAssetPermissions` | DELETE | `/api/v4/permissions/{permissionId}/` | Удалить права на ассет |
| `fetchGetAssetPermissionsTree` | GET | `/api/v4/permissions/relative/?asset_id={id}` | Дерево наследуемых прав ассета |
| `fetchAttributes` | GET | `/api/v1/attribute/` | Список атрибутов компании (пагинация, поиск, фильтр по id) |
| `createAttribute` | POST | `/api/v1/attribute/` | Создать атрибут |
| `updateAttribute` | PUT | `/api/v1/attribute/{id}/` | Обновить атрибут |
| `deleteAttribute` | DELETE | `/api/v1/attribute/{attributeId}/` | Удалить атрибут |
| `fetchModules` | GET | `/api/v1/modules/` | Список модулей (CSRF) |
### `gateway` — Gateway (ресурсы/проекты, права на ресурсы)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchProjects` | GET | `/api/v2/resources?show_all=true&limit=10000&company_id={id}` | Список проектов компании (кешируется) |
| `fetchGetProjectByProjectId` | GET | `/api/v2/resources/{projectId}` | Проект по id (кешируется) |
| `fetchCreateProject` | POST | `/api/v2/resources` | Создать проект/ресурс |
| `fetchUpdateProjectByProjectId` | PATCH | `/api/v2/resources/{projectId}` | Обновить проект |
| `fetchDeleteProjectByProjectId` | DELETE | `/api/v2/resources/{projectId}` | Удалить проект |
| `fetchParentDocumentByResourceId` | GET | `/api/v1/resources-rpc/parent-document-by-resource-id/{resourceId}` | Родительский документ по resource id |
| `fetchResources` | GET | `/api/v1/resources/?company_id={id}` | Список ресурсов компании (кешируется) |
| `fetchCreatePermission` | POST | `/api/v1/resource-permissions/` | Выдать права на ресурсы сервисному аккаунту |
| `fetchResourcesByUsersId` | POST | `/api/v1/resources/users-with-resources/` | Ресурсы по набору пользователей |
| `fetchBulkUpdateUsersPermissions` | PATCH | `/api/v1/resources/permissions-bulk/` | Массовое обновление прав на ресурсы |
| `fetchBulkUpdateUsersCompanyResourcesPermission` | POST | `/api/v1/company-resource-permissions/bulk/` | Массовая выдача прав на ресурсы компании |
### `sarex` — Локальный сервис данных
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchCoordinates` | GET | `/api/commons/cs/` | Справочник систем координат (кешируется) |
| `fetchLinksToPlanningByProjectId` | GET | `/api/pm/msp/projects/?resource_id={projectId}&strict=true` | Связи проекта с планированием (кешируется) |
| `fetchBulkUpdateUserNotifications` | PATCH | `/api/core/users/bulk/notifications/` | Массовое переключение уведомлений пользователей |
| `getMrpas` | POST | `/api/core/mrpa/list/` | Список МРПА (пагинация, фильтры, агрегации) |
| `createMrpa` | POST | `/api/core/mrpa/` | Загрузить МРПА (multipart/form-data) |
| `deleteMrpa` | DELETE | `/api/core/mrpa/{id}/` | Удалить МРПА |
### `bimv2` — BIM v2 (модели статусов)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchCreateCompanyStatusModel` | POST | `/api/v1/companies/{companyId}/status_model` | Создать модель статусов компании |
| `fetchGetCompanyStatusModels` | GET | `/api/v1/companies/{companyId}/status_model` | Модели статусов компании |
| `fetchGetBIMStatusModels` | GET | `/api/v1/bims/{bimId}/status_models` | Модели статусов BIM |
| `fetchUpdateBIMStatusModel` | POST | `/api/v1/bims/{bimId}/status_model` | Обновить модель статусов BIM |
| `fetchGetBIMStatuses` | POST | `/api/v1/bims/{bimId}/statuses?{search}` | Статусы BIM (с фильтром) |
### `premises` — Сервис помещений
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getPremises` | POST | `/api/v1/premises/filter/` | Помещения по локациям и ресурсу |
### `notifications` — Лямбда уведомлений
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchSendEmail` | POST | `/` | Отправить email-уведомление (from `hello@sarex.io`) |
## Удалённые модули (Module Federation)
Помимо HTTP-API, `srx-admin` подгружает удалённые микрофронтенды через `remoteEntry.js`. Адреса заданы в `services/admin/config/endpoints.ts` и `services/assets/config/endpoints.ts` (объект `moduleEndpoints`).
| Модуль | `stage` / `local` | `prod` | `contour` |
| --- | --- | --- | --- |
| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` | `/documentations/static/module/remoteEntry.js` |
| `assets` | `https://stage-modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `https://modules.sarex.io/control-interface/modules/assets/remoteEntry.js` | `/control-interface/modules/assets/remoteEntry.js` |
> В `preprod` используются хосты вида `https://modules.preprod.sarex.io/...`.
## Обработка ошибок и авторизация
Запросы выполняются через `httpService` (`@sarex-team/sdk-js` поверх `axios`). Для части эндпоинтов (`iam`: отделы, должности, создание пользователей/групп; `eavV1`: модули) передаётся флаг `isCSRF: true` — добавляется CSRF-токен. Ошибки обрабатываются на уровне SDK и сторов приложения; человекочитаемые сообщения задаются в сторах (`errorMessage`), например «Произошла ошибка при запросе пользователей» / «мест работы» / «ролей» / «функциональных групп».

View File

@ -0,0 +1,50 @@
# Эндпоинты, с которыми взаимодействует cross-section
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `cross-section`).
## Как устроено взаимодействие
Запросы описаны в двух API-объектах в `module/api/endpoints.ts`: `crossSectionApi` (поперечные сечения) и `exportsApi` (экспорт и скачивание вложений). Каждый метод вызывает соответствующий хелпер `httpService` (`getRequest`/`postRequest`/`deleteRequest`) со структурой:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (с подстановкой параметров/query);
- `data` — опционально, тело запроса (для `POST`/`PUT`).
`httpService` создаётся функцией `createHttpService` из `@sarex-team/sdk-js` в `module/api/http-service.ts`. Базовый хост подставляется по ключу `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV` (по умолчанию `prod`). В окружении `local` тип сервиса переключается на `original` (`setTypeOfHttpService("original")`). Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта.
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Определены окружения `local`, `stage`, `prod`, `preprod`.
| Сервис (`service`) | Назначение | `local` | `stage` | `prod` | `preprod` |
| --- | --- | --- | --- | --- | --- |
| `gateway` | Gateway/API Sarex (используется всеми эндпоинтами модуля) | `https://stage-api.sarex.io/gateway/` | `https://stage-api.sarex.io/gateway/` | `https://api.sarex.io/gateway/` | `https://api.preprod.sarex.io/gateway/` |
| `drawings` | Сервис чертежей | `https://stage-api.sarex.io/drawings/` | `https://stage-api.sarex.io/drawings/` | `https://api.sarex.io/drawings/` | `https://api.preprod.sarex.io/drawings/` |
| `sarex` | Локальный сервис данных (относительные пути) | `https://stage.sarex.io/` | `""` | `""` | `""` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | `https://login.preprod.sarex.io` |
> Фактически все эндпоинты модуля обращаются к сервису `gateway`. Сервисы `drawings`, `sarex` и `zitadel` объявлены в реестре хостов, но напрямую в `endpoints.ts` не используются.
## Эндпоинты по сервисам
### `gateway` — Gateway/API Sarex
#### `crossSectionApi` — поперечные сечения
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getCrossSections` | GET | `api/v1/drawings/cross-sections?instance_id={uuid}` | Список поперечных сечений по инстансу |
| `getCrossSectionData` | GET | `api/v1/drawings/cross-sections/{uuid}/data` | Данные поперечного сечения по uuid |
| `createCrossSections` | POST | `api/v1/drawings/cross-sections` | Создать поперечное сечение (тело — `model`) |
| `removeCrossSections` | DELETE | `api/v1/drawings/cross-sections/{uuid}/` | Удалить поперечное сечение |
#### `exportsApi` — экспорт
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `createExport` | POST | `api/v1/drawings/exports` | Создать экспорт (тело: `company_id`, `cross_section_id`, `file_type`; по умолчанию `file_type = "dwg"`) |
| `downloadExport` | GET | `api/v1/attachments/{attachment_id}` | Скачать вложение экспорта по id |
## Обработка ошибок
В модуле нет отдельного слоя маппинга ошибок (аналога `module/api/errors.ts`): обработка HTTP-ошибок выполняется на уровне `httpService` из `@sarex-team/sdk-js`. Каждый метод возвращает `response.data` (для `createExport` — весь ответ).

259
apps/django/.env.example Normal file
View File

@ -0,0 +1,259 @@
# =============================================================================
# Пример конфигурации sarex-backend (Django) + sarex-frontend
# Значения читаются кодом через django-environ (env(...)) и pydantic-settings
# (классы *Settings с env_prefix) в config/settings/*.py.
# В кластере переменные приходят из Vault (файлы /vault/secrets/*) и из блока
# env Deployment-манифестов (см. base/backend-deployment.yaml, celery-deployment.yaml).
# =============================================================================
# -----------------------------------------------------------------------------
# Django core
# -----------------------------------------------------------------------------
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_DEBUG=False
DJANGO_ISOLATED=False
ALLOWED_HOSTS='*'
APPEND_SLASH=True
FZ152_COMPLIANCE=False
PDM_SYNC=1
OBJECT_STORAGE_SYNC=True
# Секретный ключ Django. В production.py по умолчанию задан хардкодом,
# при необходимости переопределяется переменной SECRET_KEY.
SECRET_KEY=
# STATIC_ROOT / MEDIA_ROOT нужны только если раскомментированы в production.py
# STATIC_ROOT=/opt/sarex/static
# MEDIA_ROOT=/opt/sarex/media
DISK_USAGE_ROOT=/
USE_SSL_FOR_URL_SERIALIZATION=True
WEB_APP_AUTH_MODE=jwt-session-based
# -----------------------------------------------------------------------------
# База данных (PostgreSQL) — читается в config/settings/production.py
# В кластере приходит из Vault-секрета secrets/data/postgresql/apps/django
# -----------------------------------------------------------------------------
DJANGO_POSTGRES_HOST=postgresql.django.svc.cluster.local
DJANGO_POSTGRES_PORTS=5432
DJANGO_POSTGRES_DATABASE=sarex_db
DJANGO_POSTGRES_USER=sarex
DJANGO_POSTGRES_PASSWORD=password
# -----------------------------------------------------------------------------
# JWT (RS512). Ключи в кластере приходят из Vault (rsa_keys), \n экранируются.
# -----------------------------------------------------------------------------
JWT_PRIVATE_KEY=
JWT_PUBLIC_KEY=
JWT_KID=1
DJANGO_JWT_SECRET='Froom too much love of living'
SIMPLE_JWT_ISSUER=django
# -----------------------------------------------------------------------------
# Celery — брокер (RabbitMQ) и backend результатов (Redis или Postgres)
# CELERY_RABBITMQ_* приходит из Vault-секрета secrets/data/rabbitmq/apps/django
# -----------------------------------------------------------------------------
CELERY_USE_POSTGRES=False
CELERY_RABBITMQ_HOST=rabbitmq.rabbitmq.svc.cluster.local
CELERY_RABBITMQ_PORT=5672
CELERY_RABBITMQ_USER=rabbit
CELERY_RABBITMQ_PASSWORD=rabbit
CELERY_RABBITMQ_VHOST=api
CELERY_REDIS_HOST=redis
CELERY_REDIS_PORT=6379
CELERY_REDIS_DATABASE=0
# Backend результатов на Postgres (используется при CELERY_USE_POSTGRES=True)
CELERY_POSTGRES_DATABASE=celery_db
CELERY_POSTGRES_USER=sarex
CELERY_POSTGRES_PASSWORD=sarex
CELERY_POSTGRES_HOST=localhost
CELERY_POSTGRES_PORT=5432
# Дублирующий набор RabbitMQ (django), прокидывается Vault-шаблоном
DJANGO_RABBIT_HOSTNAME=rabbitmq.rabbitmq.svc.cluster.local
DJANGO_RABBIT_USER=rabbit
DJANGO_RABBIT_PASS=rabbit
DJANGO_RABBIT_VHOST=api
# Redis для Django-кеша/сервисов
DJANGO_REDIS_HOST=redis
DJANGO_REDIS_PORT=6379
# -----------------------------------------------------------------------------
# Redis-кеш (CacheSettings)
# -----------------------------------------------------------------------------
CACHE_HOST=localhost
CACHE_PORT=6379
# CACHE_PASSWORD=
CACHE_SSL=False
# CACHE_SSL_CA_CERTS=
# -----------------------------------------------------------------------------
# S3 / объектное хранилище (S3Settings, env_prefix S3_)
# S3_* приходит из Vault-секрета secrets/data/minio/apps/django
# -----------------------------------------------------------------------------
S3_HOST=https://storage.yandexcloud.net
AWS_S3_ENDPOINT_URL=https://storage.yandexcloud.net
S3_LOGIN=
S3_PASSWORD=
S3_BUCKET=sarex-media-storage
S3_REGION=ru-central1
# AWS_DEFAULT_REGION=ru-central1
# Нативная библиотека загрузки в S3 (см. Dockerfile)
S3TOOLS_LIB_PATH=/opt/sarex/lib/s3tools.so
S3TOOLS_WORKERS=10
# -----------------------------------------------------------------------------
# Kafka (KafkaSettings, env_prefix KAFKA_)
# Аутентификация приходит из Vault-секрета secrets/data/kafka/apps/django
# -----------------------------------------------------------------------------
KAFKA_BOOTSTRAP_SERVERS='["localhost:9092"]'
KAFKA_SECURITY_PROTOCOL=
KAFKA_SASL_MECHANISM=
KAFKA_SASL_PLAIN_USERNAME=user
KAFKA_SASL_PLAIN_PASSWORD=password
KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/kafka.crt
KAFKA_TOPICS='{"planning": "message-hub-stage", "ams-sync": "ams-sync"}'
# -----------------------------------------------------------------------------
# Sentry (SentrySettings, env_prefix SENTRY_) — читается из .env.base/.env
# -----------------------------------------------------------------------------
SENTRY_USE=0
SENTRY_HOST="https://sentry.sarex.io/"
SENTRY_ENVIRONMENT="production"
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=0.1
# -----------------------------------------------------------------------------
# Server-флаги приложения (ServerSettings, env_prefix SERVER_)
# Ниже — переменные, реально задаваемые в манифестах кластера.
# -----------------------------------------------------------------------------
SERVER_HOST=https://lk.sarex.io
SERVER_API_HOST=https://api.sarex.io
SERVER_ZITADEL_ENABLED=True
SERVER_KAFKA_ENABLED=False
SERVER_USE_METASHAPE=0
SERVER_USE_CLICKHOUSE=0
SERVER_USE_CHANGELOG=0
SERVER_CHANGELOG_MODE=0
SERVER_CHANGELOG_MODE_SYSTEM_LOG=1
SERVER_SAVE_DIFF_DEM=1
SERVER_S3_STREAM_IMPORT=1
SERVER_USE_DJANGO_STORAGE=1
SERVER_DJANGO_URLS=1
SERVER_CHECK_IMPORT_HASH=1
SERVER_USE_WRORKFLOW_STATUS=1
SERVER_HIDE_USER_SCROLL_PERMISSIONS=0
SERVER_EXTERNAL_FIND_BY_USERNAME_ENABLED=True
SERVER_EXTERNAL_FIND_BY_EMAIL_ENABLED=True
SERVER_CHUNKED_PATH=/tmp/chunked_uploads/%Y/%m/%d
CHECK_IMPORT_HASH=1
# -----------------------------------------------------------------------------
# Workflows / processing (WorkFlowsSettings, env_prefix WORKFLOWS_)
# -----------------------------------------------------------------------------
WORKFLOWS_USE=1
WORKFLOWS_HOST=https://api.sarex.io
WORKFLOWS_BASE_HOST=https://lk.sarex.io
WORKFLOWS_PREFIX=/internal/v1
# WORKFLOWS_TIMEOUT=120
# WORKFLOWS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q
# WORKFLOWS_TAG=stable
# -----------------------------------------------------------------------------
# Внешние API-сервисы (наследуют BaseApiServiceMixin:
# host / api_prefix / internal_host / internal_prefix / timeout / enable)
# -----------------------------------------------------------------------------
# Gateway (env_prefix GATEWAY_)
# GATEWAY_HOST=https://api.sarex.io
# Gatekeeper (env_prefix GK_)
# GK_ENCRYPTION_KEY=
# BIM v2 (env_prefix BIMV2_)
BIMV2_INTERNAL_HOST=http://bim-backend-v2-service.bim-api
BIMV2_TIMEOUT=60
# EAV (env_prefix EAV_)
EAV_ENABLE=1
# Documentation (env_prefix DOCUMENTATION_)
# DOCUMENTATION_HOST=https://api.sarex.io
# Analytics (env_prefix ANALYTICS_)
# ANALYTICS_HOST=https://lk.sarex.io
# Users (env_prefix USERS_)
# USERS_HOST=https://lk.sarex.io
# System log (env_prefix SYSTEM_LOG_)
# SYSTEM_LOG_INTERNAL_HOST=
# Resources (env_prefix RESOURCES_)
# RESOURCES_INTERNAL_HOST=http://localhost:8001
# -----------------------------------------------------------------------------
# Measurements (MeasurementSettings, env_prefix MEASUREMENTS_)
# -----------------------------------------------------------------------------
MEASUREMENTS_HOST=https://api.sarex.io/measurements/
# MEASUREMENTS_TIMEOUT=180
# MEASUREMENTS_WINDOW_SIZE=1000
# -----------------------------------------------------------------------------
# ClickHouse (ClickHouseSettings, env_prefix CLICKHOUSE_)
# -----------------------------------------------------------------------------
# CLICKHOUSE_HOST=
# CLICKHOUSE_PORT=9000
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_DATABASE=values_db
# CLICKHOUSE_TABLE=values
# CLICKHOUSE_SECURE=False
# CLICKHOUSE_VERIFY=False
# CLICKHOUSE_CERT=
# -----------------------------------------------------------------------------
# Zitadel (ZitadelSettings, env_prefix ZITADEL_)
# ZITADEL_ACCESS_TOKEN приходит из Vault-секрета secrets/data/vault/common/django_auth
# -----------------------------------------------------------------------------
ZITADEL_HOST=https://zitadel.contour.infra.sarex.tech
ZITADEL_ACCESS_TOKEN=
# ZITADEL_USERS_ENDPOINT=/v2/users
# -----------------------------------------------------------------------------
# Keycloak (KeyCloakSettings env_prefix KC_, KeyCloakSyncSettings env_prefix KC_SYNC)
# -----------------------------------------------------------------------------
KC_SYNC_ENABLE=0
KC_USE_REDIRECT_LOGOUT=False
# KC_CLIENT_ID=
# KC_CLIENT_SECRET=
# KC_DISCOVERY_URL=
# KC_REALM=sarex
# -----------------------------------------------------------------------------
# Трейсинг (TracingConfig, env_prefix TRACING_) + OpenTelemetry
# -----------------------------------------------------------------------------
# TRACING_SERVICE_NAME=backend.sarex-stage
# TRACING_ENDPOINT=localhost:4317
# TRACING_INSECURE=False
# TRACING_ENVIRONMENT=prod
# -----------------------------------------------------------------------------
# Comparator / прочее
# -----------------------------------------------------------------------------
COMPARATOR_URL=https://wb.sarex.io/comparator
COMPARATOR_SECTION=sarex-production-storage
# COMPARATOR_JWT=
# COMPARATOR_BASIC_TOKEN=
# WORKFLOWSSETTINGS_HOST=https://api.sarex.io # используется в configmap production.py
# WORKFLOWSSETTINGS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q
# -----------------------------------------------------------------------------
# Легаси/интеграции (значения по умолчанию есть в base.py)
# -----------------------------------------------------------------------------
# PG_NODE_HOST=127.0.0.1:5000
# PG_API_KEY=
# PG_MONGO_HOST=localhost
# PG_MONGO_PORT=27017
# PG_IMPORT_PATH=/home/sarex/pg/import
# WIKIMAPIA_API_KEY=
# ANALYTICS_IMPORT_METRICS_SHEET_NAME=SRX
# =============================================================================
# Frontend (sarex-frontend) — сборочные переменные (webpack DefinePlugin)
# Базовые хосты сервисов берутся из endpoints.js по ключу ENDPOINT.
# =============================================================================
# ENDPOINT=prod # stage | prod | preprod | contour | local
# SAREX_BACKEND=https://stage.sarex.io/ # прокси-таргет backend при npm start (env.js)
# TYPE_OF_HTTP_SERVICE=original
# CLIENT_ID=
# AMPLITUDE_ENABLED=false

View File

@ -0,0 +1,357 @@
# Конфигурация проекта sarex-backend (Django)
Документ описывает способы конфигурирования backend-сервиса `sarex` (Django) и
основные переменные окружения. Фронтенд-приложение `sarex-frontend` (шелл на
Module Federation) конфигурируется отдельно на этапе сборки — см. раздел в конце
и `ENDPOINTS.md`.
## Способы конфигурирования
Сервис — это Django-приложение (проект `config`, бизнес-логика в пакете `sarex`).
Конфигурация складывается из двух механизмов:
1. **Модуль настроек Django** выбирается переменной `DJANGO_SETTINGS_MODULE`.
Модули лежат в `config/settings/` и наследуются друг от друга через
`from .base import *`.
2. **Переменные окружения** читаются двумя способами:
- `django-environ` — объект `env = environ.Env()` в `config/settings/base.py`,
вызовы `env('NAME', default=...)`, `env.bool(...)`, `env.list(...)`,
`env.str(...)`;
- `pydantic-settings` — классы-наследники `BaseSettings` с `env_prefix`
(напр. `ServerSettings` → префикс `SERVER_`), инстанцируются как синглтоны
(`SERVERSETTINGS = ServerSettings()` и т.п.).
Приложение **не загружает `.env` автоматически** в основном конфиге
(`DJANGO_READ_DOT_ENV_FILE` закомментирован). Исключение — pydantic-классы
`SentrySettings` (читает `.env.base`, `.env`), `ZitadelSettings`, `KafkaSettings`
(читают `.env`). В остальном переменные нужно экспортировать в окружение процесса.
### Модули настроек (`config/settings/*.py`)
| Модуль | Назначение |
| --- | --- |
| `base.py` | Базовые настройки, все классы `*Settings`, INSTALLED_APPS, DRF, Celery-очереди |
| `production.py` | Продакшн: `DEBUG=False`, БД из `DJANGO_POSTGRES_*`, SimpleJWT (RS512), логирование |
| `docker.py` | Наследует `test.py`, `ALLOWED_HOSTS=["*"]`, БД на хосте `postgres` |
| `test.py` / `test_ksg.py` | Прогон тестов |
| `example.local.py` / `example.ldap.local.py` | Шаблоны для локального `local.py` (копируются вручную) |
По умолчанию `manage.py` и `config/celery.py` используют `config.settings.local`.
В кластере задаётся `DJANGO_SETTINGS_MODULE=config.settings.production`, при этом
файл `production.py` **подменяется** ConfigMap-ом `django-configmap` (монтируется в
`/opt/sarex/config/settings/production.py`) — см. раздел про деплой.
### Способы запуска процессов
| Процесс | Команда | Назначение |
| --- | --- | --- |
| Web/API (uWSGI) | `uwsgi --plugin python3 --ini uwsgi.ini` | HTTP API на `0.0.0.0:8000`, модуль `config.wsgi:application` |
| Web/API (dev) | `python manage.py runserver` | Локальный запуск |
| Celery worker+beat | `celery -A config worker -B -l info -E -Q default -n default_worker.%h` | Фоновые задачи и периодические таски |
| Миграции | `python manage.py migrate` | Выполняются в `entrypoint.sh` перед стартом uWSGI |
Порядок запуска контейнера backend (`entrypoint.sh`): сначала
`opentelemetry-instrument python manage.py migrate`, затем
`opentelemetry-instrument uwsgi --plugin python3 --ini uwsgi.ini`. В кластере
перед `entrypoint.sh` секреты из Vault экспортируются в окружение (`set -a; . /vault/secrets/...`).
## Переменные приложения
Ниже перечислены основные переменные. Дефолт `—` означает, что значение
обязательно (в `production.py` без него будет ошибка старта).
### Django core
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_SETTINGS_MODULE` | string | `config.settings.local` | Модуль настроек Django |
| `DJANGO_DEBUG` | bool | `False` | Режим отладки |
| `DJANGO_ISOLATED` | bool | `False` | Изолированный режим (в configmap отключает Sentry) |
| `ALLOWED_HOSTS` | list/str | — (в prod из env) | Разрешённые хосты; в кластере `*` |
| `APPEND_SLASH` | bool | `True` | Автодобавление слеша в URL |
| `FZ152_COMPLIANCE` | bool | `False` | Режим соответствия 152-ФЗ |
| `PDM_SYNC` | bool | `False` | Синхронизация с PDM |
| `OBJECT_STORAGE_SYNC` | bool | `True` | Синхронизация с объектным хранилищем |
| `SECRET_KEY` | string | хардкод в `production.py` | Секретный ключ Django |
| `SIMPLE_JWT_ISSUER` | string | `django` | Issuer для JWT |
| `DISK_USAGE_ROOT` | string | `/` | Корень для расчёта занятого места |
| `USE_SSL_FOR_URL_SERIALIZATION` | bool | `True` | Использовать https при сериализации URL |
| `WEB_APP_AUTH_MODE` | string | `JWTDefault` | Режим авторизации веб-приложения |
### База данных (PostgreSQL)
Читаются в `config/settings/production.py`. В кластере приходят из Vault-секрета
`secrets/data/postgresql/apps/django` (файл `/vault/secrets/django-postgresql`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_POSTGRES_HOST` | string | — | Хост PostgreSQL |
| `DJANGO_POSTGRES_PORTS` | string | `5432` | Порт PostgreSQL |
| `DJANGO_POSTGRES_DATABASE` | string | — | Имя базы |
| `DJANGO_POSTGRES_USER` | string | — | Пользователь |
| `DJANGO_POSTGRES_PASSWORD` | string | — | Пароль |
Движок БД — `django_prometheus.db.backends.postgresql`.
### JWT (SimpleJWT, RS512)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `JWT_PRIVATE_KEY` | string | — | Приватный RSA-ключ подписи (`\n` заменяются на переводы строк). В кластере — из Vault `rsa_keys` |
| `JWT_PUBLIC_KEY` | string | — | Публичный RSA-ключ проверки |
| `JWT_KID` | string | `None` | `kid` в заголовке токена (используется для межсервисных вызовов) |
| `DJANGO_JWT_SECRET` | string | `Froom too much love of living` | Легаси-секрет |
### Celery (`CELERY_*`)
Брокер — RabbitMQ; backend результатов — Redis (по умолчанию) или Postgres
(`CELERY_USE_POSTGRES=True`). `CELERY_RABBITMQ_*` в кластере из Vault-секрета
`secrets/data/rabbitmq/apps/django`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CELERY_USE_POSTGRES` | bool | `False` | Использовать Postgres как result backend |
| `CELERY_RABBITMQ_HOST` | string | `localhost` | Хост RabbitMQ |
| `CELERY_RABBITMQ_PORT` | int | `5672` | Порт |
| `CELERY_RABBITMQ_USER` | string | `rabbit` | Пользователь |
| `CELERY_RABBITMQ_PASSWORD` | string | `rabbit` | Пароль |
| `CELERY_RABBITMQ_VHOST` | string | `api` | Виртуальный хост |
| `CELERY_REDIS_HOST` | string | `localhost` | Хост Redis (result backend) |
| `CELERY_REDIS_PORT` | int | `6379` | Порт Redis |
| `CELERY_REDIS_DATABASE` | int | `0` | Номер БД Redis |
| `CELERY_POSTGRES_DATABASE` | string | `celery_db` | БД для result backend на Postgres |
| `CELERY_POSTGRES_USER` | string | `sarex` | Пользователь |
| `CELERY_POSTGRES_PASSWORD` | string | `sarex` | Пароль |
| `CELERY_POSTGRES_HOST` | string | `localhost` | Хост |
| `CELERY_POSTGRES_PORT` | string | `5432` | Порт |
Дополнительно из Vault-шаблона прокидываются дублирующие `DJANGO_RABBIT_HOSTNAME`,
`DJANGO_RABBIT_USER`, `DJANGO_RABBIT_PASS`, `DJANGO_RABBIT_VHOST`, а также
`DJANGO_REDIS_HOST` / `DJANGO_REDIS_PORT`.
### Кеш Redis (`CACHE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CACHE_HOST` | string | `localhost` | Хост Redis |
| `CACHE_PORT` | int | `6379` | Порт |
| `CACHE_PASSWORD` | string \| null | `None` | Пароль |
| `CACHE_SSL` | bool | `False` | TLS |
| `CACHE_SSL_CA_CERTS` | string \| null | `None` | CA-сертификат |
### S3 / объектное хранилище (`S3_*`)
`S3_*` в кластере из Vault-секрета `secrets/data/minio/apps/django`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_HOST` | string | `https://storage.yandexcloud.net` | Эндпоинт S3 |
| `S3_LOGIN` | string | `""` | Access key |
| `S3_PASSWORD` | string | `""` | Secret key |
| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию |
| `S3_REGION` | string | `""` | Регион (fallback: `AWS_DEFAULT_REGION`) |
| `AWS_S3_ENDPOINT_URL` | string | `https://storage.yandexcloud.net` | Эндпоинт (легаси-переменная) |
| `S3TOOLS_LIB_PATH` | string | `/opt/sarex/lib/s3tools.so` | Путь к нативной библиотеке загрузки (Dockerfile) |
| `S3TOOLS_WORKERS` | int | `10` | Число воркеров загрузки |
### Kafka (`KAFKA_*`)
Аутентификация в кластере из Vault-секрета `secrets/data/kafka/apps/django`.
Продюсер создаётся только при `SERVER_KAFKA_ENABLED=True`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | `["localhost:9092"]` | Список брокеров |
| `KAFKA_SECURITY_PROTOCOL` | string | `""` | Протокол безопасности |
| `KAFKA_SASL_MECHANISM` | string | `""` | SASL-механизм |
| `KAFKA_SASL_PLAIN_USERNAME` | string | `user` | Логин |
| `KAFKA_SASL_PLAIN_PASSWORD` | string | `password` | Пароль |
| `KAFKA_SSL_CAFILE` | string | `""` | Путь к CA-сертификату |
| `KAFKA_TOPICS` | dict (JSON) | `{}` | Маппинг логических имён на топики |
### Sentry (`SENTRY_*`)
Читается классом `SentrySettings` из `.env.base` / `.env`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SENTRY_USE` | bool | `True` | Включить Sentry |
| `SENTRY_HOST` | string | `""` | DSN/хост Sentry |
| `SENTRY_ENVIRONMENT` | string | `""` | Окружение |
| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов |
| `SENTRY_PROFILES_SAMPLE_RATE` | float | `0.1` | Доля профилей |
### Флаги приложения (`SERVER_*`, класс `ServerSettings`)
Класс содержит десятки булевых флагов и параметров. Наиболее значимые (реально
задаются в манифестах):
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SERVER_HOST` | string | `https://lk.sarex.io` | Внешний хост ЛК |
| `SERVER_API_HOST` | string | `https://api.sarex.io` | Внешний хост API |
| `SERVER_ZITADEL_ENABLED` | bool | `True` | Включить Zitadel-аутентификацию |
| `SERVER_KAFKA_ENABLED` | bool | `False` | Включить Kafka-продюсер |
| `SERVER_USE_METASHAPE` | bool | `True` | Использовать Metashape |
| `SERVER_USE_CLICKHOUSE` | bool | `False` | Использовать ClickHouse |
| `SERVER_CACHE_ENABLED` | bool | `False` | Включить кеш (в configmap выставляется `True`) |
| `SERVER_USE_NOTIFICATIONS` | bool | `True` | Уведомления |
| `SERVER_TIMEOUT` | int | `60` | Таймаут по умолчанию |
| `SERVER_CHUNKED_PATH` | string | — | Путь для чанкованных загрузок |
Полный список полей — в `ServerSettings` (`config/settings/base.py`). Любое поле
переопределяется переменной `SERVER_<FIELD>` в верхнем регистре.
### Workflows / processing (`WORKFLOWS_*`, класс `WorkFlowsSettings`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOWS_USE` | bool | `False` | Включить интеграцию с processing |
| `WORKFLOWS_HOST` | string | `https://api.sarex.io` | Хост сервиса processing |
| `WORKFLOWS_BASE_HOST` | string | `https://lk.sarex.io` | Базовый хост |
| `WORKFLOWS_PREFIX` | string | `/internal/v1` | Префикс внутреннего API |
| `WORKFLOWS_TIMEOUT` | int | `120` | Таймаут |
| `WORKFLOWS_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов задач |
| `WORKFLOWS_TAG` | string | `stable` | Тег образов |
### Внешние API-сервисы (`BaseApiServiceMixin`)
Классы `GateWaySetttings` (`GATEWAY_`), `BimV2ApiSettings` (`BIMV2_`),
`EAVSettings` (`EAV_`), `AnalyticsSettings` (`ANALYTICS_`),
`DocumentationSettings` (`DOCUMENTATION_`), `UsersSettings` (`USERS_`),
`SystemLogSettings` (`SYSTEM_LOG_`), `ResourceSettings` (`RESOURCES_`) наследуют
общий набор полей:
| Поле (переменная `<PREFIX>_<FIELD>`) | Тип | Назначение |
| --- | --- | --- |
| `HOST` | string | Внешний хост сервиса |
| `API_PREFIX` | string | Префикс публичного API |
| `INTERNAL_HOST` | string | Внутренний хост (внутрикластерный) |
| `INTERNAL_PREFIX` | string | Префикс внутреннего API |
| `TIMEOUT` | int | Таймаут запроса |
| `ENABLE` | bool | Включён ли сервис |
Реально задаваемые в манифестах: `BIMV2_INTERNAL_HOST`, `BIMV2_TIMEOUT`,
`EAV_ENABLE`. Отдельно — `GK_ENCRYPTION_KEY` (класс `GatekeeperSettings`).
### Measurements (`MEASUREMENTS_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `MEASUREMENTS_HOST` | string | `https://api.sarex.io/measurements/` | Хост сервиса измерений |
| `MEASUREMENTS_TIMEOUT` | int | `180` | Таймаут |
| `MEASUREMENTS_WINDOW_SIZE` | int | `1000` | Размер окна |
### ClickHouse (`CLICKHOUSE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CLICKHOUSE_HOST` | string | `rc1d-...yandexcloud.net` | Хост |
| `CLICKHOUSE_PORT` | int | `9000` | Порт |
| `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` | string | `""` | Учётные данные |
| `CLICKHOUSE_DATABASE` | string | `values_db` | База |
| `CLICKHOUSE_TABLE` | string | `values` | Таблица |
| `CLICKHOUSE_SECURE` / `CLICKHOUSE_VERIFY` | bool | `False` | TLS и проверка сертификата |
| `CLICKHOUSE_CERT` | string | `""` | CA-сертификат |
### Zitadel (`ZITADEL_*`) и Keycloak (`KC_*`, `KC_SYNC*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ZITADEL_HOST` | string | `""` | Хост Zitadel (IdP) |
| `ZITADEL_ACCESS_TOKEN` | string | `""` | Сервисный токен (из Vault `django_auth`) |
| `ZITADEL_USERS_ENDPOINT` | string | `/v2/users` | Эндпоинт пользователей |
| `KC_SYNC_ENABLE` | bool | `False` | Включить синхронизацию с Keycloak |
| `KC_USE_REDIRECT_LOGOUT` | bool | `False` | Redirect при logout |
| `KC_CLIENT_ID` / `KC_CLIENT_SECRET` / `KC_DISCOVERY_URL` / `KC_REALM` | string | см. `KeyCloakSettings` | Параметры клиента Keycloak |
### Трейсинг (`TRACING_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRACING_SERVICE_NAME` | string | `backend.sarex-stage` | Имя сервиса в трейсах |
| `TRACING_ENDPOINT` | string | `localhost:4317` | OTLP-коллектор |
| `TRACING_INSECURE` | bool | `False` | Без TLS |
| `TRACING_ENVIRONMENT` | string | `prod` | Окружение |
### Comparator и прочее
| Переменная | Значение по умолчанию | Назначение |
| --- | --- | --- |
| `COMPARATOR_URL` | `https://wb.sarex.io/comparator` | URL сервиса сравнения |
| `COMPARATOR_SECTION` | `sarex-production-storage` | Секция хранилища |
| `COMPARATOR_JWT` | `default_jwt` | Токен сравнения |
| `WORKFLOWSSETTINGS_HOST` / `WORKFLOWSSETTINGS_REGISTRY` | — | Используются напрямую в configmap `production.py` |
| `PG_NODE_HOST`, `PG_API_KEY`, `PG_MONGO_HOST`, `PG_MONGO_PORT`, `PG_IMPORT_PATH` | см. `base.py` | Легаси-интеграции PG |
## Конфигурация в кластере (Kubernetes)
Манифесты приложения — в этом же каталоге (`base/`, оверлеи `brusnika-stage`,
`brusnika-prod`, `yc-k8s-test`). Секреты монтируются через **Vault Agent Injector**
(аннотации `vault.hashicorp.com/*` на Deployment `backend` и `celery`). Файлы
секретов в контейнере и их содержимое:
| Файл `/vault/secrets/...` | Секрет Vault | Переменные |
| --- | --- | --- |
| `django-postgresql` | `secrets/data/postgresql/apps/django` | `DJANGO_POSTGRES_HOST/PORTS/DATABASE/USER/PASSWORD` |
| `django-rabbitmq` | `secrets/data/rabbitmq/apps/django` | `CELERY_RABBITMQ_*`, `DJANGO_RABBIT_*` |
| `django-s3` | `secrets/data/minio/apps/django` | `AWS_S3_ENDPOINT_URL`, `S3_HOST/BUCKET/LOGIN/PASSWORD` |
| `django-kafka` | `secrets/data/kafka/apps/django` | `KAFKA_BOOTSTRAP_SERVERS/SECURITY_PROTOCOL/SASL_*` |
| `django-jwt-private` / `django-jwt-public` | `secrets/data/vault/common/rsa_keys` | `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` |
| `django-common` | `secrets/data/vault/common/django_auth` | `ZITADEL_ACCESS_TOKEN` |
Контейнер экспортирует эти файлы в окружение до запуска (`set -a; . /vault/secrets/...`).
Кроме того, `production.py` из ConfigMap содержит функцию `_load_env_file`, которая
подхватывает те же файлы при запуске `manage.py` через `kubectl exec` вне entrypoint.
Остальные (несекретные) переменные задаются в блоке `env` контейнеров
`backend`/`celery` (`SERVER_*`, `WORKFLOWS_*`, `BIMV2_*`, `MEASUREMENTS_*`,
`ZITADEL_HOST`, `KAFKA_TOPICS`, `EAV_ENABLE`, `PDM_SYNC`, `JWT_KID` и др.).
ConfigMap `django-configmap` подменяет `config/settings/production.py`
(смонтирован в `/opt/sarex/config/settings/production.py`) и переопределяет
`ALLOWED_HOSTS`, CORS, `DATABASES`, `SIMPLE_JWT`, `REST_FRAMEWORK`, `MIDDLEWARE`,
`KeyCloakSettings`, `SAREX_MODULES`, а также включает Sentry (если не `ISOLATED`).
ConfigMap `zitadel-configmap` содержит `config.json` с `client_id`/`host` Zitadel.
`uwsgi-configmap` монтирует `uwsgi.ini`.
## CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci`
(`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение
по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE |
| --- | --- | --- |
| ветка `stage` | `stage` | `aero` |
| ветка `master` | `preprod` | (см. правила) |
| тег | `prod` | (см. правила) |
Ключевые переменные: `SERVICE_NAME=backend`, `DOCKERFILE_PATH=./Dockerfile`,
`RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `HELM_SET_ARGS`
(`universal-chart.services.backend.image.name…`, `…celery.image.name…`).
## Замечания и потенциальные проблемы
- В `production.py` `SECRET_KEY` задан хардкодом (закомментированный `env('SECRET_KEY')`).
Для реального прод-развёртывания ключ желательно вынести в секрет.
- Основной конфиг не читает `.env` автоматически; переменные нужно экспортировать
в окружение (в кластере это делает Vault + `set -a`). Только `SENTRY_*`,
`ZITADEL_*`, `KAFKA_*` читаются из файлов `.env.base`/`.env` их pydantic-классами.
- `production.py` в репозитории backend и `production.py` из ConfigMap `django-configmap`
**разные** файлы. В кластере используется версия из ConfigMap (в ней, в частности,
выставлено `DEBUG=True` в конце и включён `corsheaders`).
- `DATABASES['default']['ENGINE']``django_prometheus.db.backends.postgresql`
(обёртка для метрик Prometheus).
- Значения `SERVER_*`-флагов у `backend` и `celery` местами различаются
(напр. `SERVER_ZITADEL_ENABLED`, `SERVER_API_HOST`) — это ожидаемо.
## Минимальный набор для локального запуска
Согласно `README.md` backend: поднять Postgres/Redis/RabbitMQ (docker-compose),
скопировать шаблон настроек `cp config/settings/example.local.py config/settings/local.py`,
применить миграции (`python manage.py migrate`) и создать суперпользователя.
Минимально требуются переменные БД (`DJANGO_POSTGRES_*` или значения в `local.py`),
брокера Celery (`CELERY_RABBITMQ_*`) и, при использовании соответствующих функций,
`S3_*`, `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`. Примеры значений — в `.env.example`
рядом с этим файлом.

127
apps/django/ENDPOINTS.md Normal file
View File

@ -0,0 +1,127 @@
# Эндпоинты и хосты сервисов для sarex-frontend
Документ описывает базовые хосты сервисов, к которым обращается фронтенд-шелл
`sarex-frontend`, и удалённые модули (Module Federation), которые он подгружает.
Backend для этого приложения — `sarex-backend` (Django), его конфигурация и
переменные описаны в `CONFIGURATION.md`, а серверное REST API — в `openapi.yaml`.
## Как устроено взаимодействие
`sarex-frontend` — это host-приложение на **Webpack Module Federation**. Реестр
хостов задаётся декларативно в `endpoints.js` (корень репозитория): объект вида
`<сервис>.<тип>.<окружение>` → URL. Возможные типы:
- `api` / `apiV1` / `apiV2` / … — базовый URL REST API сервиса;
- `module` — URL `remoteEntry.js` удалённого микрофронтенда;
- `gateway` — базовый URL gateway.
Окружение выбирается сборочной переменной `ENDPOINT` (`process.env.ENDPOINT`,
по умолчанию `prod`) в `webpack.common.js`. Значения `endpoints.js[сервис][тип][ENDPOINT]`
пробрасываются в код как константы `process.env.<CONST>` через `DefinePlugin`
(объект `processEnvByEndpoint`), напр. `endpoints.bim.api[ENDPOINT]``BIM_API`.
Для локального запуска backend-таргет задаётся отдельно в `env.js`
(`SAREX_BACKEND`, по умолчанию `https://stage.sarex.io/`) и используется
dev-сервером (`webpack.dev.js`) как прокси; собственный Django-backend в окружении
`contour` доступен по относительным путям (пустой хост).
## Окружения (`ENDPOINT`)
| Значение | Назначение |
| --- | --- |
| `prod` | Продакшн (`https://api.sarex.io`, `https://modules.sarex.io`) |
| `stage` | Stage (`https://stage-api.sarex.io`, `https://stage-modules.sarex.io`) |
| `preprod` | Preprod (`https://api.preprod.sarex.io`, `https://modules.preprod.sarex.io`) |
| `contour` | Изолированный контур — относительные пути (пустой хост) |
| `local` | Локальная разработка (задан не у всех сервисов) |
> У части сервисов `gateway` определены также специальные ключи `contour_local`
> (`https://stage.sarex.io`) и `contour_prod` (`https://lk.sarex.io`).
## Базовые хосты API-сервисов
Итоговый URL = `<базовый хост сервиса>` + путь запроса.
| Сервис (`endpoints.*`) | Константа | `stage` | `prod` | `contour` |
| --- | --- | --- | --- | --- |
| `api.api` | `SAREX_API` | `https://stage-api.sarex.io` | `https://api.sarex.io` | `""` |
| `bim.api` | `BIM_API` | `https://stage-api.sarex.io/bim/api/v1` | `https://api.sarex.io/bim/api/v1` | `""` |
| `bim.apiV2` | `BIM_API_V2` | `…/bim/api/v2` | `…/bim/api/v2` | `""` |
| `bim.files` | `BIM_FILES` | `…/bim` | `…/bim` | `""` |
| `workspaces.api` / `workspacesV2.api` | `WORKSPACESV2_API` | `…/workspaces/` | `…/workspaces/` | `/workspaces/` |
| `workflows.api` | `WORKFLOWS_API` | `…/workflows` | `…/workflows` | `/workflows` |
| `remarks.api` | `REMARKS_API` | `…/remarks/api/v1` | `…/remarks/api/v1` | `/remarks/api/v1` |
| `issues.api` | `ISSUES_API` | `…/issues/api/v1` | `…/issues/api/v1` | `/issues/api/v1` |
| `issuesBase.api` | `ISSUES_BASE_API` | `…/issues/api` | `…/issues/api` | `/issues/api` |
| `flows.api` | — | `…/issues/api/v1` | `…/issues/api/v1` | `/issues/api/v1` |
| `inspections.api` | — | `…/inspections/api/v1` | `…/inspections/api/v1` | `/inspections/api` |
| `documentations.api` | `DOCUMENTATIONS_API` | `…/documentations/api/v1` | `…/documentations/api/v1` | `/documentations/api/v1` |
| `pm.api` | — | `…/documentations/api/v1` | `…/documentations/api/v1` | `/documentations/api/v1` |
| `analyticsV2.api` | — | `…/analytics-v2/api/v1` | `…/analytics-v2/api/v1` | `/analytics-v2/api/v1` |
| `analytics.api` | `ANALYTICS_API` | `…/analytics` | `…/analytics` | `/analytics` |
| `processes.api` | `PROCESSES_API` | `…/flows/api/v1` | `…/flows/api/v1` | `/flows/api/v1` |
| `gateway.gateway` | `GATEWAY` | `…/gateway` | `…/gateway` | (local: `http://localhost:9000/gateway`) |
| `gateway.api` | `GATEWAY_API` | `…/gateway/api/v1` | `…/gateway/api/v1` | `/gateway/api/v1` |
| `gateway.apiV2` | `GATEWAY_API_V2` | `…/gateway/api/v2` | `…/gateway/api/v2` | `/gateway/api/v2` |
| `eav.api` | `EAV_API` | `…/eav/api/v0` | `…/eav/api/v0` | `/eav/api/v0` |
| `eav.apiV1…apiV4` | `EAV_API_V1…V4` | `…/eav/api/v1…v4` | `…/eav/api/v1…v4` | `/eav/api/v1…v4` |
| `notifications.api` | `NOTIFICATIONS_API` | `…/lambdas/notification/` | `…/lambdas/notification/` | `""` |
| `lambdas.api` | `LAMBDAS_API` | `…/lambdas` | `…/lambdas` | `/lambdas` |
| `automations.api` | `AUTOMATIONS_API` | `…/automation/api/v1` | `…/automation/api/v1` | `/automation/api/v1` |
| `orchestrator.api` | `ORCHESTRATOR_API` | `…/orchestrator` | `…/orchestrator/api` | `/orchestrator/api` |
> `…` = `https://stage-api.sarex.io` (stage) или `https://api.sarex.io` (prod).
> Собственный Django-backend (`sarex-backend`) в контуре обслуживается по
> относительным путям `/api/...` — см. `openapi.yaml`.
## Удалённые модули (Module Federation, `remoteEntry.js`)
Хост подгружает микрофронтенды по URL из `endpoints.<сервис>.module[ENDPOINT]`.
Базовый хост модулей: `https://stage-modules.sarex.io` (stage) /
`https://modules.sarex.io` (prod); в контуре — относительные пути.
| Сервис | Константа | Путь `module` (относительный, контур) |
| --- | --- | --- |
| `workspaces` | `WORKSPACES_MODULE` | `/workspaces/module/remoteEntry.js` |
| `workspacesV2` | `WORKSPACESV2_MODULE` | `/workspaces-v2/module/remoteEntry.js` |
| `workflows` | `WORKFLOWS_MODULE` | `/workflows/module/remoteEntry.js` |
| `remarks` | `REMARKS_MODULE` | `/remarks/static/module/remoteEntry.js` |
| `issues` | `ISSUES_MODULE` | `/issues/static/module/remoteEntry.js` |
| `flows` | `FLOWS_MODULE` | `/flows/static/module/remoteEntry.js` |
| `inspections` | `INSPECTIONS_MODULE` | `/inspections/static/module/remoteEntry.js` |
| `documentations` | `DOCUMENTATIONS_MODULE` | `/documentations/static/module/remoteEntry.js` |
| `pm` | `PM_MODULE` | `/pm/module/remoteEntry.js` |
| `projects` | `PROJECTS_MODULE` | `/projects/static/module/remoteEntry.js` |
| `analyticsV2` | `ANALYTICS_MODULE` | `/analytics-v2/static/module/remoteEntry.js` |
| `reviews` | `REVIEWS_MODULE` | `/reviews/static/module/remoteEntry.js` |
| `administration` | `ADMINISTRATION_MODULE` | `/control-interface/modules/admin/remoteEntry.js` |
| `adminProc` | `ADMIN_PROC_MODULE` | `/admin-frontend/static/module/remoteEntry.js` |
| `assets` | `ASSETS_MODULE` | `/control-interface/modules/assets/remoteEntry.js` |
| `premises` | `PREMISES_MODULE` | `/premises/static/module/remoteEntry.js` |
| `contracts` | `CONTRACTS_MODULE` | `/cotracts/static/module/remoteEntry.js` |
| `transmittal` | `TRANSMITTAL_MODULE` | `/transmittal/static/module/remoteEntry.js` |
| `prescriptions` | `PRESCRIPTIONS_MODULE` | `/prescriptions/static/module/remoteEntry.js` |
| `rfi` | `RFI_MODULE` | `/rfi/static/module/remoteEntry.js` |
| `assistant` | — | `/assistant/static/module/remoteEntry.js` |
Список подключаемых в ЛК модулей (пункты меню) дублируется на стороне backend в
`SAREX_MODULES` (ConfigMap `django-configmap`): `remarks`, `issues`,
`documentations`, `reviews`, `processes`, `rfi`, `transmittal`.
## Backend, обслуживающий шелл
Основной API самого шелла (аутентификация, пользователи, настройки приложения,
проекты/цели/миссии, аналитика) — это `sarex-backend` под префиксом `/api/...`
`/internal/...` для внутрикластерных вызовов). Полное описание серверных
эндпоинтов приведено в `openapi.yaml`. Ключевые группы:
| Префикс | Назначение |
| --- | --- |
| `/api/token…`, `/api/auth/…`, `/api/login`, `/api/logout` | Аутентификация и JWT |
| `/api/app-settings/` | Настройки приложения |
| `/api/core/…` | Пользователи, компании, цели, миссии, ортофото, облака точек и т.д. |
| `/api/client/…` | Клиентский дашборд, загрузки, self-сервис |
| `/api/analytics/…` | Аналитические дашборды, метрики, виджеты |
| `/api/map/…` | Кадастр и заметки на карте |
| `/api/pg/…` | Облака точек, экспорт, измерения |
| `/internal/client/…` | Внутренние вызовы (настройки, токены) |

View File

@ -0,0 +1,417 @@
# Полный перечень запросов sarex-frontend
Извлечено из исходников `src/` и `modules/`: обёртки `httpService.{get,post,put,patch,delete}Request`, вызовы `fetch`, эндпоинты RTK Query (`builder.query/mutation`). Базовый хост подставляется по ключу `service` (для `httpService`, см. `src/Model/api/hosts.ts`) либо по `env.*` из `endpoints.js`; выбор окружения — переменной сборки `ENDPOINT`. В путях `{param}` — подстановки, `?a=&b=` — query-параметры, заданные в коде. Хосты приведены для `prod`.
**Всего вызовов: 392** — httpService: 296, fetch: 76, RTK Query: 20.
## `sarex` — Django backend (`sarex-backend`), host `/` (в контуре относительные пути)
Уникальных запросов: 206
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+2) |
| POST | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+1) |
| PUT | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts (+3) |
| PATCH | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts |
| DELETE | `(динамический URL — передаётся переменной)` | src/Model/BaseApi.ts |
| POST | `/api/analytics/attachments/` | src/Components/ImageDropZone/ImageDropZone.jsx |
| GET | `/api/analytics/attachments/?dashboard={dashboardId}` | src/Components/ImageDropZone/hook.js |
| DELETE | `/api/analytics/attachments/{id}/` | src/Components/ImageDropZone/ImageDropZone.jsx |
| DELETE | `/api/analytics/attributes-options/{id}/` | src/Model/api.js |
| DELETE | `/api/analytics/attributes/{id}/` | src/Model/api.js |
| PUT | `/api/analytics/dashboards/{id}/` | src/Model/api.js |
| DELETE | `/api/analytics/dashboards/{id}/` | src/Model/api.js |
| GET | `/api/analytics/expressions/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts |
| POST | `/api/analytics/expressions/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts |
| DELETE | `/api/analytics/expressions/{id}/` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts |
| PUT | `/api/analytics/expressions/{newComputationValue.id}` | modules/analytics/entities/ComputationValue/ComputationValueEntity.ts |
| PATCH | `/api/analytics/folders/{body.id}/` | modules/analytics/store/api/api.ts |
| DELETE | `/api/analytics/folders/{id}/` | modules/analytics/store/api/api.ts (+1) |
| PATCH | `/api/analytics/groups/{body.id}/` | src/Model/api.js |
| DELETE | `/api/analytics/groups/{id}/` | src/Model/api.js |
| DELETE | `/api/analytics/metric2widget/{id}/` | src/Model/api.js |
| GET | `/api/analytics/metrics/` | modules/analytics/entities/Metric/MetricEntity.ts |
| DELETE | `/api/analytics/metrics/{id}/` | modules/analytics/store/api/api.ts |
| PUT | `/api/analytics/metrics/{metric.id}/` | modules/analytics/store/api/api.ts |
| GET | `/api/analytics/metrics/{params \|\|` | modules/analytics/store/api/api.ts |
| GET | `/api/analytics/texts/?dashboard={id}` | modules/widget/store/widgets.ts |
| PATCH | `/api/analytics/texts/{body.id}/` | src/Model/api.js |
| DELETE | `/api/analytics/texts/{id}/` | src/Model/api.js |
| POST | `/api/analytics/values/import_xlsx/` | modules/analytics/store/api/api.ts |
| PATCH | `/api/analytics/values/{id}/` | src/Model/api.js |
| DELETE | `/api/analytics/values/{valueId}/` | src/Model/api.js |
| PATCH | `/api/analytics/widgets/{id}/` | src/Model/api.js |
| DELETE | `/api/analytics/widgets/{id}/` | src/Model/api.js |
| GET | `/api/client/dashboard/feed/` | src/Model/actions.js |
| GET | `/api/client/dashboard/targets/{orderId}/feed/` | src/Model/actions.js |
| POST | `/api/client/dashboard/targets/{orderId}/feed/` | src/Model/actions.js |
| POST | `/api/client/dashboard/targets/{targetId}/request_survey/` | src/Model/actions.js |
| GET | `/api/client/dashboard/webcams/` | src/Model/api.js |
| GET | `/api/client/folders/?target={targetId}&limit=10000` | src/Model/api.js |
| PATCH | `/api/client/folders/{id}/` | src/Model/api.js |
| DELETE | `/api/client/folders/{id}/` | src/Model/api.js |
| GET | `/api/client/settings/` | src/Model/actions.js |
| PUT | `/api/client/settings/` | src/Model/actions.js |
| POST | `/api/client/uploads/` | src/Model/actions.js |
| GET | `/api/client/uploads/?target={targetId}&limit=10000` | src/Model/actions.js |
| GET | `/api/client/uploads/{doc}/layers/` | src/Model/actions.js |
| DELETE | `/api/client/uploads/{fileId}/` | src/Model/actions.js |
| POST | `/api/client/uploads/{fileId}/translate_to_svf/` | src/Model/actions.js |
| PATCH | `/api/client/uploads/{id}/` | src/Model/actions.js |
| GET | `/api/commons/cs/` | src/Model/actions.js (+1) |
| GET | `/api/core/admin/companies/` | src/shared/api/fetch/company.api.ts |
| POST | `/api/core/c2s-comparisons/` | src/Components/Viewer/Panels/ComparisonsPanel/store/api/api.ts |
| GET | `/api/core/companies/` | modules/targetsTree/repositories/CompanyRepository/RESTCompanyRepository.ts (+1) |
| POST | `/api/core/contour/export/` | src/Model/api.js |
| GET | `/api/core/contour/export/?pointcloud={pointcloudId}` | src/Model/api.js |
| GET | `/api/core/materials/` | modules/measurements/components/volume/store/materials.ts |
| POST | `/api/core/media-folders/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| DELETE | `/api/core/media-folders/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| PATCH | `/api/core/media-folders/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| POST | `/api/core/mesh/` | src/Model/api.js |
| DELETE | `/api/core/mesh/{id}/` | src/Model/api.js |
| POST | `/api/core/missions/import/create/` | src/Model/actions.js |
| GET | `/api/core/missions/{id}/` | src/Model/api.js |
| POST | `/api/core/mrpa/` | src/shared/api/fetch/mrpa.api.ts |
| POST | `/api/core/mrpa/list/` | src/shared/api/fetch/mrpa.api.ts |
| DELETE | `/api/core/mrpa/{id}/` | src/shared/api/fetch/mrpa.api.ts |
| GET | `/api/core/orthophotos/{id}/` | src/Model/api.js |
| GET | `/api/core/orthophotos/{orthoId}/altitude_and_temperature/?points={lng},{lat}` | src/Model/api.js |
| GET | `/api/core/panoramas/{id}/` | src/Model/api.js |
| DELETE | `/api/core/panoramas/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| PATCH | `/api/core/panoramas/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| DELETE | `/api/core/photos/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| PATCH | `/api/core/photos/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| GET | `/api/core/pointclouds/?mission={mission}&comparison_type={type}` | src/Components/Viewer/Panels/ComparisonsPanel/store/api/api.ts |
| POST | `/api/core/polygons/` | src/Model/actions.js |
| GET | `/api/core/polygons/?with_links_folder{targetIdQuery}` | src/Model/api.js |
| POST | `/api/core/state/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts |
| GET | `/api/core/state/?pointcloud_id={pointCloudId}` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts |
| PATCH | `/api/core/state/{id}/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts |
| DELETE | `/api/core/state/{id}/` | src/Components/Viewer/Services/States/Api/ViewerStatesApi.ts |
| GET | `/api/core/targets/{id}/` | src/Model/api.js |
| GET | `/api/core/users/` | modules/dashboards/models/entities/Users/UsersEntity.ts |
| DELETE | `/api/core/videos/{id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| PATCH | `/api/core/videos/{params.id}/` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| GET | `/api/map/cadastre/point/?point={point}` | src/Model/actions.js |
| GET | `/api/map/notes/folders/?orthophoto={targetId}` | src/Model/api.js |
| PATCH | `/api/map/notes/folders/{id}/` | src/Model/api.js |
| DELETE | `/api/map/notes/folders/{id}/` | src/Model/api.js |
| PATCH | `/api/mar/tasks/{taskId}/` | src/Model/api.js |
| DELETE | `/api/mar/tasks_attachments/{id}/` | src/Model/api.js |
| DELETE | `/api/mar/tasks_groups_attachments/{id}/` | src/Model/api.js |
| GET | `/api/notifications` | modules/targetsTree/repositories/TargetRepository/RESTTragetRepository.ts (+1) |
| POST | `/api/pg/attachments/` | src/Model/actions.js (+1) |
| DELETE | `/api/pg/attachments/{id}/` | src/Model/api.js |
| POST | `/api/pg/compare/c2c/` | src/Model/api.js |
| GET | `/api/pg/compare/c2c/?pointcloud={pointcloud}` | src/Model/api.js |
| POST | `/api/pg/compare/c2s/` | src/Model/actions.js |
| POST | `/api/pg/compare/t2t/` | src/Model/api.js |
| GET | `/api/pg/compare/t2t/?pointcloud={pointcloud}` | src/Model/api.js |
| POST | `/api/pg/export/pointcloud/` | src/Model/api.js |
| GET | `/api/pg/export/pointcloud/?pointcloud={pointcloudId}` | src/Model/api.js |
| POST | `/api/pg/measurements/` | src/Model/Store/voxelVolumeStore.js (+1) |
| GET | `/api/pg/measurements/?target={targetId}` | src/Model/actions.js |
| PUT | `/api/pg/measurements/{idOnServer}/` | src/Model/Store/voxelVolumeStore.js |
| DELETE | `/api/pg/measurements/{idOnServer}/` | src/Model/Store/voxelVolumeStore.js |
| PUT | `/api/pg/measurements/{measurement.idOnServer}/` | src/Model/actions.js |
| DELETE | `/api/pg/measurements/{measurementId}/` | src/Model/actions.js |
| GET | `/api/pg/orthomosaicexport/?mission_id={missionId}` | src/Model/api.js |
| POST | `/api/pg/pdf-overlays/` | src/Model/api.js |
| DELETE | `/api/pg/pdf-overlays/{id}/` | src/Model/api.js |
| GET | `/api/pg/pointclouds/{id}/comparisons/` | src/Model/api.js |
| GET | `/api/pg/pointclouds/{pointCloudId}/` | src/Model/actions.js |
| GET | `/api/pg/pointclouds/{pointCloudId}/altitude/?points={formattedPoints}` | src/Model/actions.js |
| POST | `/api/pg/pointclouds/{pointCloudId}/baseplane_volume/` | src/Model/actions.js |
| POST | `/api/pg/pointclouds/{pointCloudId}/measurements/` | src/Model/actions.js |
| GET | `/api/pg/pointclouds/{pointCloudId}/measurements/delete_measurements/` | src/Model/actions.js |
| DELETE | `/api/pg/pointclouds/{pointCloudId}/measurements/{idOnServer}/` | src/Model/actions.js |
| PATCH | `/api/pg/pointclouds/{pointCloudId}/measurements/{measurementId}/` | src/Model/actions.js |
| POST | `/api/pg/pointclouds/{pointCloudId}/project_volume/` | src/Model/actions.js |
| POST | `/api/pg/pointclouds/{pointCloudId}/volume/` | src/Model/actions.js |
| GET | `/api/pg/pointclouds/{pointcloud}/comparisons/` | src/Model/api.js |
| POST | `/api/pg/pointclouds/{targetId}/measurements/{measurementId}/calculate/` | src/Model/api.js |
| GET | `/api/pg/projects/` | src/Model/actions.js |
| POST | `/api/pg/projects/` | src/Model/actions.js |
| POST | `/api/pg/projects/{id}/images/` | src/Model/actions.js |
| PATCH | `/api/pg/projects/{id}/images/{photo.id}/` | src/Model/actions.js |
| GET | `/api/pg/projects/{id}/status/` | src/Model/actions.js |
| POST | `/api/pg/projects/{projectId}/build/` | src/Model/actions.js |
| GET | `/api/pg/projects/{projectId}/images/{GCPName ? `?marker={GCPName}` :` | src/Model/actions.js |
| POST | `/api/pg/projects/{projectId}/init/` | src/Model/actions.js |
| GET | `/api/pg/projects/{projectId}/markers/` | src/Model/actions.js |
| POST | `/api/pg/projects/{projectId}/optimize_cameras/` | src/Model/actions.js |
| POST | `/api/pg/projects/{projectId}/update_marker/` | src/Model/actions.js |
| GET | `/api/pm/dms/?target={targetId}` | src/Model/actions.js |
| GET | `/api/pm/msp/projects/?bundle_id={bundleID}&schema=tiny` | modules/pm/gate/repositories/SelectedKSGProjectRepository/WorkspaceSelectedKSGProjectRepository.ts |
| DELETE | `/api/pm/msp/projects/{id}/` | src/Model/api.js |
| GET | `/api/pm/msp/projects/{id}/profiles/` | modules/pm/gate/repositories/TaskResourceConnectionBaseRepository/TaskResourceConnectionBaseRepository.ts |
| GET | `/api/pm/msp/resources-tasks/?projects={project}` | modules/pm/gate/repositories/TaskResourcesConnectionsRepository/TaskResourcesConnectionsRepository.ts |
| GET | `/api/pm/msp/tasks/?project={project}{query ? `&{query}` :` | modules/pm/gate/repositories/TasksRepository/TasksRepository.ts |
| GET | `/api/pm/msp/tasks/{id}/` | modules/pm/gate/repositories/TasksRepository/TasksRepository.ts |
| GET | `/api/workflows/?target={id}` | modules/targetsV2_OLD_DEPRECATED/services/TargetTreeService/TargetTreeService.ts |
| GET | `/api/workflows/?target={this.targetId}` | modules/missions/controllers/MissionsCardList/MissionsCardListController.ts |
| POST | `Analytic.baseUrl` | src/Model/api.js |
| POST | `ClientFolders.baseUrl` | src/Model/api.js |
| GET | `DashboardsAPI.baseUrl` | src/Model/api.js |
| POST | `DashboardsAPI.baseUrl` | src/Model/api.js |
| POST | `GroupsApi.baseUrl` | src/Model/api.js |
| POST | `MetricsApi.baseUrl` | modules/analytics/store/api/api.ts |
| GET | `MetricsFoldersAPI.baseUrl` | modules/analytics/store/api/api.ts |
| POST | `MetricsFoldersAPI.baseUrl` | modules/analytics/store/api/api.ts |
| POST | `NotesFolders.baseUrl` | src/Model/api.js |
| POST | `ProjectsAPI.taskUrl` | src/Model/api.js |
| PUT | `StreamFile.baseUrl` | src/Model/api.js |
| GET | `host` | modules/control/entities/Storage/StorageRESTEntity.ts |
| POST | `host` | modules/control/entities/Storage/StorageRESTEntity.ts |
| GET | `hostGetInfo` | modules/control/entities/Storage/StorageRESTEntity.ts |
| GET | `hostInfo` | modules/account/entities/Storage/StorageRESTEntity.ts |
| POST | `hostInfo` | modules/account/entities/Storage/StorageRESTEntity.ts |
| POST | `hostUpdatePassword` | modules/account/entities/Storage/StorageRESTEntity.ts |
| GET | `hosts` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| POST | `hosts` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| POST | `hosts.expression2widget` | modules/widget/entities/Expressions/ExpressionEntity.ts |
| GET | `hosts.expressions` | modules/widget/entities/Expressions/ExpressionEntity.ts |
| GET | `link` | src/Model/actions.js |
| POST | `mediaEndpoint` | src/Model/Media/api.ts |
| GET | `nextUrl` | src/Model/MediaSidebar/Api/MediaModelApi.ts |
| POST | `panoramasEndpoint` | src/Model/Media/api.ts |
| POST | `photosEndpoint` | src/Model/Media/api.ts |
| GET | `this.path` | modules/analytics/entities/Folder/FolderEntity.ts |
| GET | `this.pathname` | modules/missions/entity/Mission/MissionRESTEntity.ts |
| GET | `this.props.url` | modules/auth/TokenIssuer.ts |
| POST | `this.uploadUrl` | src/Model/streamFile.ts |
| GET | `url` | src/Model/api.js |
| GET | `url.toString()` | modules/targetsTree/repositories/TargetRepository/RESTTragetRepository.ts (+2) |
| POST | `videosEndpoint` | src/Model/Media/api.ts |
| POST | `{Export.baseUrlHorizontal}` | src/Model/api.js |
| POST | `{Export.urlTablePoints}` | src/Model/api.js |
| POST | `{Export.urlTablePoints}pointcloud/` | src/Model/api.js |
| PATCH | `{ProjectsAPI.taskUrl}{body.id}/` | src/Model/api.js |
| DELETE | `{ProjectsAPI.taskUrl}{id}/` | src/Model/api.js |
| GET | `{TrackingMetaData.trackerUrl}events/` | src/Model/api.js |
| GET | `{hosts.expression2widget}?widget={id}` | modules/widget/entities/Expressions/ExpressionEntity.ts |
| PUT | `{hosts.expression2widget}{body.id}/` | modules/widget/entities/Expressions/ExpressionEntity.ts |
| DELETE | `{hosts.expression2widget}{id}/` | modules/widget/entities/Expressions/ExpressionEntity.ts |
| GET | `{hosts}aggregates/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| POST | `{hosts}{body.dashboboardId}/copy/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| PUT | `{hosts}{body.id}/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| DELETE | `{hosts}{id}/` | modules/dashboards/models/entities/Dashboards/DashboardsEntity.ts |
| GET | `{host}api/tracking/meta/contractor/` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}api/tracking/meta/vehicle/` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}api/tracking/meta/vehicletracker/` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}api/tracking/meta/zones/` | modules/vehicle-tracking/methods/inner/api.ts |
| POST | `{host}api/tracking/meta/zones/` | modules/vehicle-tracking/methods/inner/api.ts |
| PUT | `{host}api/tracking/meta/zones/{id}` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}api/tracking/stats-zoned/?{queryParams.toString()}` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}buffer/` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{host}entries-log/?{queryParams.toString()}` | modules/vehicle-tracking/methods/inner/api.ts |
| GET | `{panoramasEndpoint}?mission={mission}` | src/Model/Media/api.ts |
| PATCH | `{panoramasEndpoint}{panoramaId}` | src/Model/Media/api.ts |
| GET | `{photosEndpoint}?mission={mission}` | src/Model/Media/api.ts |
| PATCH | `{photosEndpoint}{photoId}/` | src/Model/Media/api.ts |
| GET | `{this.pathname}?{searchParams.toString()}` | modules/targets/entities/Target/TargetRESTEntity.ts |
| PATCH | `{this.pathname}{params.id}/` | modules/missions/entity/Mission/MissionRESTEntity.ts |
| DELETE | `{this.pathname}{params.id}/` | modules/missions/entity/Mission/MissionRESTEntity.ts |
| GET | `{videosEndpoint}?mission={mission}` | src/Model/Media/api.ts |
| PATCH | `{videosEndpoint}{videoId}` | src/Model/Media/api.ts |
## `sarexApi``https://api.sarex.io` (stage `https://stage-api.sarex.io`)
Уникальных запросов: 23
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `(динамический URL — передаётся переменной)` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+3) |
| GET | `/flows/api/v1/{entity}/tasks-count/` | src/Model/actions.js |
| GET | `/transmittals/api/v1/transmittals/count` | src/Model/actions.js |
| GET | `pulse/api/core/check_admin/` | src/Model/Pulse/api.ts |
| GET | `pulse/api/core/user/` | src/Model/Pulse/api.ts |
| GET | `{baseUrl}/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts (+1) |
| POST | `{baseUrl}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+2) |
| DELETE | `{baseUrl}/bulk_delete/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) |
| GET | `{baseUrl}/check_new/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts |
| GET | `{baseUrl}/last-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts |
| POST | `{baseUrl}/mark-all-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts |
| POST | `{baseUrl}/mark-viewed` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts |
| GET | `{baseUrl}/unread-count` | modules/sarexPulse/SarexPulse/changelog/api/notificationApi.ts |
| POST | `{baseUrl}/upload` | modules/sarexPulse/SarexPulse/shared/api/mediaApi.ts |
| POST | `{baseUrl}/viewed/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts (+1) |
| GET | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) |
| PUT | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) |
| DELETE | `{baseUrl}/{id}/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+2) |
| POST | `{baseUrl}/{id}/close/` | modules/sarexPulse/SarexPulse/banners/api/bannersPublicApi.ts |
| POST | `{baseUrl}/{id}/dislike/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts |
| POST | `{baseUrl}/{id}/like/` | modules/sarexPulse/SarexPulse/changelog/api/changelogPublicApi.ts |
| POST | `{baseUrl}/{id}/publish/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) |
| POST | `{baseUrl}/{id}/unpublish/` | modules/sarexPulse/SarexPulse/banners/api/bannersAdminApi.ts (+1) |
## `bim``https://api.sarex.io/bim`
Уникальных запросов: 17
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| PATCH | `/api/v1/bims/{bimId}` | src/Model/BIM/api.js |
| POST | `/api/v1/bims/{bimId}/archive` | src/Model/BIM/api.js |
| GET | `/api/v1/bims/{bimId}/changes` | src/Model/BIM/api.js |
| POST | `/api/v1/bims/{bimId}/changes` | src/Model/BIM/api.js |
| GET | `/api/v1/bims/{bimId}/deviations` | src/Model/BIM/api.js |
| GET | `/api/v1/bims/{bimId}/elements` | src/Model/BIM/api.js |
| GET | `/api/v1/bims/{bimId}/sarexid/{sarexIds}` | src/Model/BIM/api.js |
| POST | `/api/v1/bims/{bimId}/unarchive` | src/Model/BIM/api.js |
| POST | `/api/v1/bims/{srcBIMId}/merge/{dstBIMId}` | src/Model/BIM/api.js |
| POST | `/api/v1/changes` | src/Model/BIM/api.js |
| POST | `/api/v1/comparisons` | src/Model/BIM/api.js |
| GET | `/api/v1/elements/{elementId}/properties` | src/Model/BIM/api.js |
| PUT | `/api/v1/elements/{elementId}/properties` | src/Model/BIM/api.js |
| GET | `/api/v1/elements/{elementId}/stats` | src/Model/BIM/api.js |
| POST | `/api/v1/targets/{targetId}/bims` | src/Model/BIM/api.js |
| GET | `/api/v1/targets/{targetId}/bims?archived=1` | src/Model/BIM/api.js |
| POST | `/api/v2/deviations` | src/Model/BIM/api.js |
## `workflows``https://api.sarex.io/workflows`
Уникальных запросов: 2
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `/api/v1/workflows/{comparison.c2c_workflow},{comparison.t2t_workflow}/state` | src/Components/Viewer/Panels/ComparisonsPanel/components/MissionComparison/MissionComparison.tsx |
| GET | `/api/v1/workflows/{workflowId}` | src/Components/Viewer/Panels/FilesPanel/hooks/useGetWorkflowStatus.ts |
## `workspaces``https://api.sarex.io/workspaces`
Уникальных запросов: 1
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `/api/v1/workspaces/{workspaceID}` | modules/dashboards/view-model/WorkspacePreviewSelectorViewModel/WorkspacePreviewSelectorViewModel.ts |
## `gateway``https://api.sarex.io/gateway`
Уникальных запросов: 1
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `/api/v1/resources` | src/Model/actions.js |
## `comparisons``https://api.sarex.io/comparisons`
Уникальных запросов: 1
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `/{id}/` | src/Components/Viewer/Panels/LayersPanel/store/comparisonsLayers.ts |
## `notifications``https://api.sarex.io/lambdas/notification`
Уникальных запросов: 1
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| POST | `(динамический URL — передаётся переменной)` | modules/notifications/email-notification/email-notifier.ts |
## `processes` (fetch) — `https://api.sarex.io/flows/api/v1`
Уникальных запросов: 32
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| POST | `/documents/` | modules/reviews/api/index.ts |
| GET | `/documents/?full=true{document_ids ? `&document_ids={document_ids}` :` | modules/reviews/api/index.ts |
| PATCH | `/documents/set-status/?document_ids={ids.join(",")}` | modules/reviews/api/index.ts |
| PUT | `/documents/{id}/` | modules/reviews/api/index.ts |
| POST | `/flows/` | modules/processes/store/api/index.ts |
| GET | `/flows/count_flows_by_resource/?{params}` | modules/processes/store/api/index.ts |
| PUT | `/flows/{body.id}/?full=true` | modules/processes/store/api/index.ts |
| POST | `/flows/{id}/copy/?full=true` | modules/processes/store/api/index.ts |
| POST | `/reviewers/` | modules/processes/store/api/index.ts |
| PUT | `/reviewers/{body.id}/` | modules/processes/store/api/index.ts |
| DELETE | `/reviewers/{id}/` | modules/processes/store/api/index.ts |
| POST | `/reviews/` | modules/reviews/api/index.ts |
| GET | `/reviews/count_by_resource_id/?{params}` | modules/reviews/api/index.ts |
| GET | `/reviews/count_by_reviewer_id/?current_reviewers={query}` | modules/reviews/api/index.ts |
| PUT | `/reviews/{body.id}/` | modules/reviews/api/index.ts |
| DELETE | `/reviews/{id}/` | modules/reviews/api/index.ts |
| PATCH | `/reviews/{id}/approve/` | modules/reviews/api/index.ts |
| GET | `/reviews/{id}/documents/` | modules/reviews/api/index.ts |
| PATCH | `/reviews/{id}/update-bundles/` | modules/reviews/api/index.ts |
| PATCH | `/reviews/{reviewId}/change_reviewers/` | modules/reviews/api/index.ts |
| PUT | `/reviews/{reviewId}/documents/` | modules/reviews/api/index.ts |
| POST | `/statuses/` | modules/processes/store/api/index.ts |
| PUT | `/statuses/{body.id}/` | modules/processes/store/api/index.ts |
| DELETE | `/statuses/{id}/` | modules/processes/store/api/index.ts |
| POST | `/steps/` | modules/processes/store/api/index.ts |
| PUT | `/steps/{body.id}/?full=true` | modules/processes/store/api/index.ts |
| GET | `/steps/{stepId}/active_reviews/` | modules/processes/store/api/index.ts |
| GET | `/steps/{stepId}/get_reviewers/?review_id={reviewId}` | modules/reviews/api/index.ts |
| PATCH | `/steps/{stepId}/update_reviewers/` | modules/processes/store/api/index.ts |
| GET | `/tasks/reviewers-max-end-dates/?{query}` | modules/reviews/api/index.ts |
| PATCH | `/tasks/{id}/change-duration/` | modules/reviews/api/index.ts |
| PATCH | `/tasks/{id}/change-priority/` | modules/reviews/api/index.ts |
## `orchestrator` (fetch) — `https://api.sarex.io/orchestrator/api`
Уникальных запросов: 3
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| POST | `/process` | modules/reviews/api/marks.ts |
| GET | `/process/{id}` | modules/reviews/api/marks.ts |
| POST | `/sign` | modules/reviews/api/marks.ts |
## `automation` (fetch) — `https://api.sarex.io/automation/api/v1`
Уникальных запросов: 1
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| POST | `/automations` | modules/automation/store/automation.ts |
## `analytics-v2` (RTK Query) — `https://api.sarex.io/analytics-v2/api/v1`
Уникальных запросов: 19
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `/api/commons/wiki.get` | src/Components/Wiki/apiWiki/api.ts |
| POST | `/api/commons/wiki.media.save` | src/Components/Wiki/apiWiki/api.ts |
| POST | `/api/commons/wiki.update` | src/Components/Wiki/apiWiki/api.ts |
| GET | `/api/core/media-notes-attachments/` | src/Model/MediaNotes/api.ts |
| POST | `/api/core/media-notes-attachments/` | src/Model/MediaNotes/api.ts |
| DELETE | `/api/core/media-notes-attachments/{attachmentId}` | src/Model/MediaNotes/api.ts |
| GET | `/api/core/media-notes-comments/` | src/Model/MediaNotes/api.ts |
| POST | `/api/core/media-notes-comments/` | src/Model/MediaNotes/api.ts |
| DELETE | `/api/core/media-notes-comments/{body.commentId}/` | src/Model/MediaNotes/api.ts |
| GET | `/api/core/media-notes/` | src/Model/MediaNotes/api.ts |
| POST | `/api/core/media-notes/` | src/Model/MediaNotes/api.ts |
| PATCH | `/api/core/media-notes/{body.id}/` | src/Model/MediaNotes/api.ts |
| DELETE | `/api/core/media-notes/{params.noteId}/` | src/Model/MediaNotes/api.ts |
| POST | `/api/core/targets/v2/{target_id}/calculate_panoramas_relative_orientation/` | src/Components/MediaViewer/MediaPanorama/panoramaApi.ts |
| GET | `/api/core/targets/{target}/missions/` | src/Components/Viewer/Panels/Media/MediaApi/missionsApi.ts |
| GET | `/api/notifications/?page={page}` | src/Model/AeroNoty/api.ts |
| POST | `/api/notifications/update/` | src/Model/AeroNoty/api.ts |
| GET | `/workflows/get_items_by_generic_key/?model=target&object_id={target_id}` | src/Components/MediaViewer/MediaPanorama/panoramaApi.ts |
| GET | `wfs` | src/Model/GIS/api.ts |
## Прочее (static-конфиг, внешние сервисы, wrappers с динамическим URL)
Уникальных запросов: 13
| Метод | Путь | Файл (источник) |
| --- | --- | --- |
| GET | `(динамический URL — передаётся переменной)` | src/Components/Viewer/Panels/Media/MediaRedux.tsx |
| GET | `/static/config.json` | src/shared/config/config.ts |
| GET | `<processes>/tasks/?` + params.toString()` | modules/reviews/api/index.ts |
| GET | `file.attachment` | src/Components/Viewer/Panels/MeasurementDetails/MediaFiles_old.jsx |
| GET | `http://localhost:8000{path}` | src/Model/fakeServer/fakeServer.js |
| GET | `https://blooming-cove-51473.herokuapp.com/track/list` | src/Model/Store/trackingStore.js |
| GET | `https://blooming-cove-51473.herokuapp.com/track/notifications` | src/Model/Store/trackingStore.js |
| GET | `image` | src/Components/Viewer/Services/States/Stores/ViewerStates.ts |
| GET | `link` | src/Model/actions.js |
| GET | `url` | modules/account/ui/StorageCard/lib/file-utils.ts (+2) |
| POST | `url` | modules/reviews/api/index.ts |
| PUT | `url` | modules/reviews/api/index.ts |
| PATCH | `url` | modules/reviews/api/index.ts |

589
apps/django/openapi.yaml Normal file
View File

@ -0,0 +1,589 @@
openapi: 3.0.3
info:
title: Sarex Backend API
version: "1.1.12"
description: |
REST API сервиса **sarex-backend** — монолитное Django-приложение (проект
`config`, бизнес-логика в пакете `sarex`) на Django REST Framework. Отдаётся
через uWSGI (`config.wsgi:application`) на порту `8000`.
Документ описывает основную поверхность публичного API под префиксом `/api/`
и внутреннего API под префиксом `/internal/`. Маршрутизация собирается в
`config/urls.py` и включаемых `sarex/*/api/urls.py`. Многие ресурсы
зарегистрированы через DRF-роутеры (`SimpleRouter`/`DefaultRouter`), поэтому
поддерживают стандартный набор действий (list/create/retrieve/update/
partial_update/destroy). Конкретные схемы запросов/ответов в коде не
объявлены декларативно (используется `rest_framework.schemas.coreapi.AutoSchema`),
поэтому тела здесь описаны обобщённо.
### Аутентификация
Большинство эндпоинтов требуют аутентификации (DRF
`DEFAULT_PERMISSION_CLASSES = [IsAuthenticated]`). Поддерживаются несколько
механизмов (`DEFAULT_AUTHENTICATION_CLASSES`): Zitadel JWT, SimpleJWT
(`Authorization: Bearer <jwt>`, алгоритм `RS512`), Basic, Session,
RemoteUser. Токены выпускаются эндпоинтами `/api/token…`.
### Пагинация
По умолчанию используется `LimitOffsetPagination` (`PAGE_SIZE = 1000`).
Списочные ответы содержат `count`, `next`, `previous`, `results`.
### Замечание о полноте
Перечислены основные маршруты. Часть включаемых подмодулей
(`sarex/pg/api/*`, `sarex/mar/*`, `sarex/base/api/commons`) представлена
группами; детальные под-пути см. в соответствующих `urls.py`.
servers:
- url: https://api.sarex.io
description: prod
- url: https://stage-api.sarex.io
description: stage
- url: /
description: contour (относительные пути)
security:
- bearerAuth: []
tags:
- name: auth
description: Аутентификация и JWT-токены
- name: base
description: Уведомления, workflows, модули, health
- name: core
description: Пользователи, компании, цели, миссии, медиа
- name: client
description: Клиентский дашборд и self-сервис
- name: analytics
description: Дашборды, метрики, виджеты
- name: map
description: Кадастр и заметки на карте
- name: pg
description: Облака точек, экспорт, измерения
- name: internal
description: Внутрикластерные вызовы
- name: system
description: Метрики и служебные эндпоинты
paths:
# ---------------------------------------------------------------------------
# Auth / tokens
# ---------------------------------------------------------------------------
/api/login/:
post:
tags: [auth]
summary: Вход пользователя (сессия)
security: []
responses:
"200": { $ref: "#/components/responses/Ok" }
"401": { $ref: "#/components/responses/Unauthorized" }
/api/logout/:
post:
tags: [auth]
summary: Выход пользователя
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/app-settings/:
get:
tags: [auth]
summary: Настройки приложения
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/token/:
post:
tags: [auth]
summary: Получить пару access/refresh токенов
security: []
requestBody: { $ref: "#/components/requestBodies/Generic" }
responses:
"200": { $ref: "#/components/responses/TokenPair" }
"401": { $ref: "#/components/responses/Unauthorized" }
/api/token/me:
post:
tags: [auth]
summary: Токен для текущего пользователя
responses:
"200": { $ref: "#/components/responses/TokenPair" }
/api/token/user/{pk}/:
post:
tags: [auth]
summary: Токен для пользователя по id (из админки)
parameters: [ { $ref: "#/components/parameters/PkPath" } ]
responses:
"200": { $ref: "#/components/responses/TokenPair" }
/api/token/jwks:
get:
tags: [auth]
summary: JWKS (набор публичных ключей)
security: []
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/token/refresh/:
post:
tags: [auth]
summary: Обновить access-токен (ротация)
security: []
requestBody: { $ref: "#/components/requestBodies/Generic" }
responses:
"200": { $ref: "#/components/responses/TokenPair" }
/api/token/public/:
get:
tags: [auth]
summary: Публичный ключ проверки JWT
security: []
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/auth/obtain/:
post:
tags: [auth]
summary: Получить refresh-токен
security: []
requestBody: { $ref: "#/components/requestBodies/Generic" }
responses:
"200": { $ref: "#/components/responses/TokenPair" }
/api/auth/refresh/:
post:
tags: [auth]
summary: Обновить access-токен
security: []
requestBody: { $ref: "#/components/requestBodies/Generic" }
responses:
"200": { $ref: "#/components/responses/TokenPair" }
# ---------------------------------------------------------------------------
# System
# ---------------------------------------------------------------------------
/metrics:
get:
tags: [system]
summary: Метрики Prometheus
security: []
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/health/:
get:
tags: [base]
summary: Health check
security: []
responses:
"200": { $ref: "#/components/responses/Ok" }
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
/api/modules/:
get:
tags: [base]
summary: Список доступных модулей
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/update/notifications/:
post:
tags: [base]
summary: Массовое обновление уведомлений
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/workflows/:
get:
tags: [base]
summary: Список workflow
responses:
"200": { $ref: "#/components/responses/List" }
/api/workflows/{id}/:
parameters: [ { $ref: "#/components/parameters/IdPath" } ]
get:
tags: [base]
summary: Workflow по id
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/notifications/:
get:
tags: [base]
summary: Список уведомлений
responses:
"200": { $ref: "#/components/responses/List" }
/api/notifications/{id}/:
parameters: [ { $ref: "#/components/parameters/IdPath" } ]
get:
tags: [base]
summary: Уведомление по id
responses:
"200": { $ref: "#/components/responses/Ok" }
/api/usernotifications/:
get:
tags: [base]
summary: Пользовательские уведомления
responses:
"200": { $ref: "#/components/responses/List" }
/api/commons/cs/:
get:
tags: [base]
summary: Справочник систем координат
responses:
"200": { $ref: "#/components/responses/List" }
# ---------------------------------------------------------------------------
# Core — ресурсы DRF-роутера (CRUD)
# ---------------------------------------------------------------------------
/api/core/users/:
get:
tags: [core]
summary: Список пользователей
responses: { "200": { $ref: "#/components/responses/List" } }
post:
tags: [core]
summary: Создать пользователя
requestBody: { $ref: "#/components/requestBodies/Generic" }
responses: { "201": { $ref: "#/components/responses/Ok" } }
/api/core/users/{id}/:
parameters: [ { $ref: "#/components/parameters/IdPath" } ]
get: { tags: [core], summary: Пользователь по id, responses: { "200": { $ref: "#/components/responses/Ok" } } }
put: { tags: [core], summary: Обновить пользователя, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "200": { $ref: "#/components/responses/Ok" } } }
patch: { tags: [core], summary: Частично обновить пользователя, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "200": { $ref: "#/components/responses/Ok" } } }
delete: { tags: [core], summary: Удалить пользователя, responses: { "204": { description: No Content } } }
/api/core/users/introspect:
get:
tags: [core]
summary: Интроспекция текущего пользователя
responses: { "200": { $ref: "#/components/responses/Ok" } }
/api/core/users/{pk}/introspect:
parameters: [ { $ref: "#/components/parameters/PkPath" } ]
get:
tags: [core]
summary: Интроспекция пользователя (админ)
responses: { "200": { $ref: "#/components/responses/Ok" } }
/api/core/users/bulk/notifications/:
post:
tags: [core]
summary: Массовое обновление уведомлений пользователей
responses: { "200": { $ref: "#/components/responses/Ok" } }
/api/core/users_by_sa/:
get:
tags: [core]
summary: Пользователи по сервисному аккаунту
responses: { "200": { $ref: "#/components/responses/List" } }
/api/core/v2/users/:
get:
tags: [core]
summary: Упрощённый список пользователей (v2)
responses: { "200": { $ref: "#/components/responses/List" } }
/api/core/v3/users/:
get:
tags: [core]
summary: Оптимизированный список пользователей (v3)
responses: { "200": { $ref: "#/components/responses/List" } }
/api/core/companies/:
get: { tags: [core], summary: Список компаний, responses: { "200": { $ref: "#/components/responses/List" } } }
post: { tags: [core], summary: Создать компанию, requestBody: { $ref: "#/components/requestBodies/Generic" }, responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/companies/{id}/:
parameters: [ { $ref: "#/components/parameters/IdPath" } ]
get: { tags: [core], summary: Компания по id, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/surfaces/:
get: { tags: [core], summary: Список поверхностей, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/mesh/:
get: { tags: [core], summary: Список mesh, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/multiple-surface/:
get: { tags: [core], summary: Множественные поверхности, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/state/:
get: { tags: [core], summary: Состояние приложения, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/polygons/:
get: { tags: [core], summary: Полигоны, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/media-notes/:
get: { tags: [core], summary: Медиа-заметки, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/media-notes-comments/:
get: { tags: [core], summary: Комментарии медиа-заметок, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/media-notes-attachments/:
get: { tags: [core], summary: Вложения медиа-заметок, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/media-folders/:
get: { tags: [core], summary: Папки медиа, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/target-links/:
get: { tags: [core], summary: Ссылки на цели, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/materials/:
get: { tags: [core], summary: Материалы, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/pointclouds/:
get: { tags: [core], summary: Облака точек, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/orthophotos/:
get: { tags: [core], summary: Ортофотопланы, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/videos/:
get: { tags: [core], summary: Видео, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/photos/:
get: { tags: [core], summary: Фото, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/panoramas/:
get: { tags: [core], summary: Панорамы, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/color-presets/:
get: { tags: [core], summary: Цветовые пресеты компании, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/missions/:
get: { tags: [core], summary: Список миссий, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/missions/import/:
get: { tags: [core], summary: Задачи импорта миссий, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/missions/import/create/:
post: { tags: [core], summary: Создать задачу импорта миссии, responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/targets/:
get: { tags: [core], summary: Список целей, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/targets/v2/:
get: { tags: [core], summary: Список целей (v2), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/targets/v3/:
get: { tags: [core], summary: Список целей (v3), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/targets/tree/:
get: { tags: [core], summary: Дерево целей, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/targets/pdm/:
get: { tags: [core], summary: Цели PDM, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/targets/{id}/missions/:
parameters: [ { $ref: "#/components/parameters/IdPath" } ]
get: { tags: [core], summary: Миссии цели, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/export/:
get: { tags: [core], summary: Экспорт данных, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/export/pointcloud/:
post: { tags: [core], summary: Экспорт региона из облака точек, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/contour/export/:
post: { tags: [core], summary: Экспорт контура, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/contour/exports/:
get: { tags: [core], summary: Список экспортов контура, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/uploads/streaming/:
post: { tags: [core], summary: Потоковая загрузка (создание), responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/uploads/streaming/{pk}/:
parameters: [ { $ref: "#/components/parameters/PkStrPath" } ]
put: { tags: [core], summary: Догрузка чанка, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/storage-cleanup/:
post: { tags: [core], summary: Очистка хранилища, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/celery/stats:
get: { tags: [core], summary: Статистика Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/celery/active:
get: { tags: [core], summary: Активные задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/celery/scheduled:
get: { tags: [core], summary: Запланированные задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/celery/{uuid}/details/:
parameters: [ { name: uuid, in: path, required: true, schema: { type: string } } ]
get: { tags: [core], summary: Детали задачи Celery, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/admin/users/:
get: { tags: [core], summary: Админ — пользователи, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/groups/:
get: { tags: [core], summary: Админ — группы, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/companies/:
get: { tags: [core], summary: Админ — компании, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/positions/:
get: { tags: [core], summary: Админ — должности, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/departments/:
get: { tags: [core], summary: Админ — подразделения, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/contractors/:
get: { tags: [core], summary: Админ — подрядчики, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/admin/permissions/:
get: { tags: [core], summary: Админ — права, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/service_accounts/:
get: { tags: [core], summary: Сервисные аккаунты, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/service-accounts/personalized/:
get: { tags: [core], summary: Персонализированный сервисный аккаунт, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/service-accounts/user/{user_id}/:
parameters: [ { name: user_id, in: path, required: true, schema: { type: integer } } ]
get: { tags: [core], summary: Сервисный аккаунт пользователя (v2), responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/core/c2s-comparisons/:
post: { tags: [core], summary: Создать c2s-сравнение, responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/permissions/:
get: { tags: [core], summary: Права (только чтение), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/units/:
get: { tags: [core], summary: Единицы измерения, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/upload/media-file/:
post: { tags: [core], summary: Загрузка медиа-файла, responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/mrpa/list/:
get: { tags: [core], summary: Список MRPA, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/core/mrpa/:
post: { tags: [core], summary: Создать MRPA, responses: { "201": { $ref: "#/components/responses/Ok" } } }
/api/core/mrpa/{pk}/:
parameters: [ { name: pk, in: path, required: true, schema: { type: string, format: uuid } } ]
get: { tags: [core], summary: MRPA по id, responses: { "200": { $ref: "#/components/responses/Ok" } } }
# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------
/api/client/dashboard/targets/:
get: { tags: [client], summary: Дашборд — цели, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/client/dashboard/missions/:
get: { tags: [client], summary: Дашборд — миссии, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/client/dashboard/webcams/:
get: { tags: [client], summary: Дашборд — веб-камеры, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/client/settings/:
get: { tags: [client], summary: Настройки пользователя, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/client/settings/user/{user_id}/:
parameters: [ { name: user_id, in: path, required: true, schema: { type: integer } } ]
get: { tags: [client], summary: Настройки пользователя по id, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/client/self/:
get: { tags: [client], summary: Информация о себе, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/client/self/set_password/:
post: { tags: [client], summary: Смена пароля, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/client/uploads/:
get: { tags: [client], summary: Загрузки клиента, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/client/folders/:
get: { tags: [client], summary: Папки загрузок клиента, responses: { "200": { $ref: "#/components/responses/List" } } }
# ---------------------------------------------------------------------------
# Analytics
# ---------------------------------------------------------------------------
/api/analytics/dashboards/:
get: { tags: [analytics], summary: Дашборды, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/widgets/:
get: { tags: [analytics], summary: Виджеты, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/metrics/:
get: { tags: [analytics], summary: Метрики, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/expressions/:
get: { tags: [analytics], summary: Выражения, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/folders/:
get: { tags: [analytics], summary: Папки метрик, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/filters/:
get: { tags: [analytics], summary: Фильтры, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/groups/:
get: { tags: [analytics], summary: Группы, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/values/:
get: { tags: [analytics], summary: Значения метрик, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/attributes/:
get: { tags: [analytics], summary: Атрибуты, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/attachments/:
get: { tags: [analytics], summary: Вложения дашбордов, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/analytics/reviews-service-feed/:
post: { tags: [analytics], summary: Приём событий из сервиса reviews, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/analytics/remarks-service-feed/:
post: { tags: [analytics], summary: Приём событий из сервиса remarks, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/analytics/tracking-service-feed/:
post: { tags: [analytics], summary: Приём событий трекинга, responses: { "200": { $ref: "#/components/responses/Ok" } } }
# ---------------------------------------------------------------------------
# Map
# ---------------------------------------------------------------------------
/api/map/cadastre/point/:
get: { tags: [map], summary: Данные кадастра по точке, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/map/cadastre/export/:
post: { tags: [map], summary: Экспорт кадастровых данных, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/map/cadastre/wikimapia/redirect/:
get: { tags: [map], summary: Редирект Wikimapia, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/map/notes/:
get: { tags: [map], summary: Заметки на ортофото, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/map/notes/folders/:
get: { tags: [map], summary: Папки заметок на ортофото, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/ds/telecom/cadastre/:
get: { tags: [map], summary: Изображение кадастра (telecom), responses: { "200": { $ref: "#/components/responses/Ok" } } }
# ---------------------------------------------------------------------------
# PG (облака точек / экспорт / измерения)
# ---------------------------------------------------------------------------
/api/pg/pointclouds/:
get: { tags: [pg], summary: Облака точек, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/pointclouds/{pointcloud}/measurements/:
parameters: [ { name: pointcloud, in: path, required: true, schema: { type: string } } ]
get: { tags: [pg], summary: Измерения облака точек, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/volume-dynamic/:
get: { tags: [pg], summary: Динамика объёмов, responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/pg/orthomosaicexport/:
get: { tags: [pg], summary: Экспорт ортомозаики (кастомный), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/exports/pointcloud/:
get: { tags: [pg], summary: Экспорты облаков точек, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/exports/orthomosaic/:
get: { tags: [pg], summary: Экспорты ортомозаики, responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/measurements/:
get: { tags: [pg], summary: Измерения (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/attachments/:
get: { tags: [pg], summary: Вложения (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/compare/:
get: { tags: [pg], summary: Сравнение (подмодуль), responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/pg/export/:
get: { tags: [pg], summary: Экспорт (подмодуль), responses: { "200": { $ref: "#/components/responses/Ok" } } }
/api/pg/pdf-overlays/:
get: { tags: [pg], summary: PDF-оверлеи (подмодуль), responses: { "200": { $ref: "#/components/responses/List" } } }
/api/pg/projects/:
get: { tags: [pg], summary: Проекты Metashape (при SERVER_USE_METASHAPE), responses: { "200": { $ref: "#/components/responses/List" } } }
# ---------------------------------------------------------------------------
# Internal
# ---------------------------------------------------------------------------
/internal/client/settings/{pk}/:
parameters: [ { $ref: "#/components/parameters/PkPath" } ]
get:
tags: [internal]
summary: Внутренние настройки клиента
responses: { "200": { $ref: "#/components/responses/Ok" } }
/internal/client/token/{pk}/:
parameters: [ { $ref: "#/components/parameters/PkPath" } ]
get:
tags: [internal]
summary: Внутренний токен клиента
responses: { "200": { $ref: "#/components/responses/TokenPair" } }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
IdPath:
name: id
in: path
required: true
schema: { type: integer }
PkPath:
name: pk
in: path
required: true
schema: { type: integer }
PkStrPath:
name: pk
in: path
required: true
schema: { type: string }
requestBodies:
Generic:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
Ok:
description: Успешный ответ
content:
application/json:
schema:
type: object
additionalProperties: true
List:
description: Списочный ответ с пагинацией (LimitOffset)
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedList"
TokenPair:
description: Пара токенов
content:
application/json:
schema:
$ref: "#/components/schemas/TokenPair"
Unauthorized:
description: Не аутентифицирован
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
schemas:
PaginatedList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true, format: uri }
previous: { type: string, nullable: true, format: uri }
results:
type: array
items:
type: object
additionalProperties: true
TokenPair:
type: object
properties:
access: { type: string }
refresh: { type: string }
Error:
type: object
properties:
detail: { type: string }

View File

@ -0,0 +1,16 @@
# document-link-frontend (Next.js)
#
# Внимание: текущий код фронтенда НЕ читает переменные окружения напрямую
# (в src/ нет обращений к process.env / NEXT_PUBLIC_*). Базовый URL API
# выбирается по window.location.hostname, а JWT зашит в коде (см. CONFIGURATION.md).
# Перечисленные ниже переменные — это конфигурационная поверхность, объявленная
# в Helm (.helm/values.yaml), и их следует использовать вместо хардкода.
# Публичный базовый хост API (сервис documentations), без схемы.
# Итоговый запрос: https://<NEXT_PUBLIC_API_BASE_URL>/documentations/api/v1/public/documents/public_link/<uuid>
# stage: stage-api.sarex.io, production: api.sarex.io
NEXT_PUBLIC_API_BASE_URL=stage-api.sarex.io
# JWT сервисного аккаунта для запроса публичной ссылки (Authorization: Bearer <jwt>).
# В инфраструктуре берётся из секрета documentations-publiclink-jwt-secret (ключ jwt).
NEXT_PUBLIC_API_TOKEN=

View File

@ -0,0 +1,95 @@
# Конфигурация проекта document-link (document-link-frontend)
Документ описывает способы конфигурирования и все переменные окружения сервиса публичных ссылок на документы.
## Что это за сервис
`document-link-frontend` — микрофронтенд на **Next.js 13** (App Router, `src/app`), отдающий публичную страницу-карточку документа по ссылке вида `https://document-link.<env>.sarex.io/<uuid>`. По `uuid` фронтенд запрашивает метаданные документа у сервиса `documentations` и показывает название, автора, версию, размер, срок действия ссылки и кнопку скачивания (см. `ENDPOINTS.md`). Собственного бэкенда у сервиса нет — это чистый фронтенд, поэтому файлы вида `openapi.yaml` для него неприменимы.
## Способы конфигурирования
В отличие от бэкенд-сервисов, у фронтенда **нет разбора переменных окружения в коде**. На текущий момент:
- Базовый хост API выбирается в рантайме по `window.location.hostname` в `src/app/[uuid]/components/modal.tsx` (жёстко заданный `switch`), а не из переменной окружения;
- JWT для авторизации запроса **зашит в коде** (константа `fixedToken` в том же файле) — временное решение;
- Обращений к `process.env` / `NEXT_PUBLIC_*` в `src/` нет.
При этом конфигурационная поверхность **объявлена в инфраструктуре** (Helm-чарт репозитория и CI) в виде переменных `NEXT_PUBLIC_*` — их предполагается использовать вместо хардкода. Ниже описаны и код, и инфраструктурные объявления.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся значения |
| --- | --- |
| Локально (`npm run dev`) | Значения зашиты в коде; `apiBaseUrl` для `localhost``stage-api.sarex.io` |
| Локально (Docker / `docker-compose`) | `Dockerfile` (`node:18-alpine`, `next build`, `ENTRYPOINT npm start`, порт `3000`); `docker-compose.yaml` пробрасывает `8000:3000` |
| Kubernetes — репозиторный чарт | `.helm/values.yaml`: блок `services.frontend.envs` и `secretEnvs` (см. ниже) |
| Kubernetes — infra (этот репозиторий) | `apps/document-link/base` (kustomize) и `apps/document-link/{brusnika-stage,brusnika-prod}` (FluxCD `HelmRelease` поверх `universal-chart`) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: `generic/common-ci` (`universal-pipeline`, ref `apps-business`), `SERVICE_NAME=document-link` |
## Переменные приложения
Объявлены в `.helm/values.yaml` исходного репозитория. **Важно:** текущий код фронтенда их не читает (см. раздел «Замечания»).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `NEXT_PUBLIC_API_BASE_URL` | string | `stage-api.sarex.io` (stage), `api.sarex.io` (production) | Базовый хост API (сервис `documentations`), без схемы |
| `NEXT_PUBLIC_API_TOKEN` | string (secret) | — | JWT сервисного аккаунта для `Authorization: Bearer <jwt>`. Берётся из секрета `documentations-publiclink-jwt-secret` (ключ `jwt`) |
## Базовые хосты API по окружениям
Логика `src/app/[uuid]/components/modal.tsx` (`switch` по `window.location.hostname`):
| Hostname фронтенда | `apiBaseUrl` |
| --- | --- |
| `localhost` | `stage-api.sarex.io` |
| `document-link.stage.sarex.io` | `stage-api.sarex.io` |
| `document-link.sarex.io` | `api.sarex.io` |
| прочее | не определён (ошибка в консоль, fallback `stage-api.sarex.io`) |
## Сборка и контейнер
| Параметр | Значение | Где задано |
| --- | --- | --- |
| Базовый образ | `node:18-alpine` | `Dockerfile` |
| Команда сборки | `npm i``npm run build` (`next build`) | `Dockerfile` |
| Entrypoint | `npm start` (`next start`) | `Dockerfile` |
| Порт приложения | `3000` | `Dockerfile` (`EXPOSE 3000`), `.helm/values.yaml` (`port._default: 3000`) |
| Образ (registry) | `cr.yandex/crp3ccidau046kdj8g9q/document-link-frontend` | `.helm/values.yaml`, infra `HelmRelease` |
## Деплой (infra: `apps/document-link`)
| Оверлей | Механизм | Особенности |
| --- | --- | --- |
| `base` | kustomize (`Deployment` + `Service` + `Namespace`) | namespace `document-link` c `istio-injection: enabled`; `Deployment/frontend` |
| `brusnika-stage` | FluxCD `HelmRelease``universal-chart` `0.1.7` | `replicaCount`: stage `1`; probes выключены |
| `brusnika-prod` | FluxCD `HelmRelease``universal-chart` `0.1.7` | `replicaCount`: preprod/production `3`; probes выключены |
| `yc-k8s-test` | kustomize (`../base`) | тестовый контур |
Порты сервиса: в `universal-chart``service.port 8080``targetPort 3000` (`portName: http`); в `base/service.yaml``port 80``targetPort 80`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Ключевые переменные: `SERVICE_NAME=document-link`, `DOCKERFILE_PATH=./Dockerfile`, `CI_TRIGGER_SOURCE=app`. Окружение переключается по ветке/тегу:
| Условие | STAND | Namespace | Chart |
| --- | --- | --- | --- |
| `merge_request_event` | — | — | `ENABLE_BUILD_IMAGE=false` (только проверки) |
| ветка `stage` | `stage` | `documentations` | `document-link` `0.1.7`, `--build-arg ENV=stage` |
| ветка `master` | `preprod` | `document-link-preprod` | `document-link` `0.1.7`, `--build-arg ENV=preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `document-link-prod` | `document-link` `0.1.7`, `--build-arg ENV=prod` |
`HELM_SET_ARGS` пробрасывает в `universal-chart` образ (`services.frontend.image.name.<env>`), `global.env`, а также `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner`.
## Замечания и потенциальные проблемы
- **Env-переменные не используются кодом.** `NEXT_PUBLIC_API_BASE_URL` и `NEXT_PUBLIC_API_TOKEN` объявлены в `.helm/values.yaml`, но `src/` их не читает: базовый хост берётся из `switch` по `hostname`, а токен зашит константой `fixedToken`. Для корректной работы на разных стендах логику стоит перевести на `process.env.NEXT_PUBLIC_*` (и тогда `.env.example` станет рабочим шаблоном).
- **Зашитый JWT.** Константа `fixedToken` в `modal.tsx` — секрет в исходниках и с ограниченным сроком действия (`exp`). Должен приходить из секрета `documentations-publiclink-jwt-secret` через `NEXT_PUBLIC_API_TOKEN`.
- **Неизвестный hostname.** При домене, не входящем в `switch`, `apiBaseUrl` не задаётся явно (используется дефолт `stage-api.sarex.io`) — для новых стендов список нужно расширять.
- **Расхождение портов.** Приложение слушает `3000` (Dockerfile/helm `targetPort`), но `apps/document-link/base/deployment.yaml` объявляет `containerPort: 80`, а `base/service.yaml``port/targetPort 80`. В `universal-chart` (`HelmRelease`) — корректный `targetPort 3000`. Kustomize-`base` стоит выровнять на `3000`.
- **Многоступенчатый Dockerfile закомментирован.** Финальный `runner`-stage отключён — образ запускается из `builder` с полным `node_modules`; для прод-образа стоит включить slim-runner.
## Минимальный набор для запуска
- Локально: `npm i && npm run dev`, открыть `http://localhost:3000/<uuid>` (API — `stage-api.sarex.io`).
- Docker: `docker compose up` (порт `8000` → контейнер `3000`).
- В кластере фактически требуется рабочий JWT для сервиса `documentations` (сейчас — `fixedToken`; целевое — секрет `documentations-publiclink-jwt-secret`).

View File

@ -0,0 +1,66 @@
# Эндпоинты, с которыми взаимодействует document-link-frontend
Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается фронтенд публичных ссылок (`document-link-frontend`).
## Как устроено взаимодействие
Фронтенд загружает карточку документа по `uuid` из URL (`/<uuid>`). Запрос выполняется хуком `useSWR` в `src/app/[uuid]/components/modal.tsx` через нативный `fetch`. Базовый хост API выбирается в рантайме по `window.location.hostname`, итоговый URL = `https://<apiBaseUrl>` + путь эндпоинта. Скачивание файлов выполняется переходом браузера по ссылкам, которые возвращает сам API (`download_link`, `download_mrpas_link`).
Авторизация: заголовок `Authorization: Bearer <jwt>` (сейчас — зашитая константа `fixedToken`; целевое — `NEXT_PUBLIC_API_TOKEN` из секрета `documentations-publiclink-jwt-secret`). Запрос идёт с `credentials: "include"`.
## Базовые хосты по окружениям
Значения из `switch` по `window.location.hostname` в `modal.tsx`:
| Hostname фронтенда | `apiBaseUrl` (базовый хост API) |
| --- | --- |
| `localhost` | `https://stage-api.sarex.io` |
| `document-link.stage.sarex.io` | `https://stage-api.sarex.io` |
| `document-link.sarex.io` | `https://api.sarex.io` |
| прочее | не определён (fallback `https://stage-api.sarex.io`) |
## Эндпоинты по сервисам
### `documentations` — Сервис документации
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| Публичная ссылка | GET | `/documentations/api/v1/public/documents/public_link/{uuid}` | Метаданные документа по публичной ссылке (`uuid`) |
Пример итогового URL: `https://stage-api.sarex.io/documentations/api/v1/public/documents/public_link/e602b98d-58a1-4ba3-8a89-84e5617b5aac`.
Ожидаемые поля ответа (используются во фронтенде):
| Поле ответа | Тип | Использование |
| --- | --- | --- |
| `name` | string | Название документа |
| `document_type` | string | Тип (иконка): `bim`/`bimv2`/`cloud`/`surface`/`workspace`/`pdf`/`deviation`/`c2s`/`c2c`/`abap`/`ksg`/`docx`/`xlsx`/`dxf`/`dwg`/… |
| `author` | string | Автор |
| `version` | string | Версия документа (скрывается для `workspace`/`folder`/`project`) |
| `size` | number | Размер в байтах (форматируется библиотекой `bytes`) |
| `download_token` | string | Токен скачивания |
| `download_link` | string (URL) | Прямая ссылка на скачивание файла |
| `download_mrpas_link` | string (URL) \| null | Ссылка на скачивание МЧД (опционально) |
| `expires_at` | string (datetime) \| null | Срок действия ссылки; `null` → «Неограничено» |
| `is_connector` | bool | Признак «файл > 5 Гб, требуется Sarex-коннектор» |
### Скачивание файлов (динамические ссылки)
Не отдельные эндпоинты реестра, а переход браузера по URL из ответа:
| Действие | Источник URL |
| --- | --- |
| Скачать файл/папку | `download_link` из ответа `public_link` |
| Скачать МЧД | `download_mrpas_link` из ответа `public_link` (если не `null`) |
При `is_connector = true` вместо прямого скачивания показывается предупреждение со ссылкой на [Sarex-коннектор](https://support.sarex.io/knowledge_base/item/347946).
## Обработка ошибок
Статус ответа маппится в человекочитаемое сообщение (`modal.tsx`):
| Статус | Сообщение |
| --- | --- |
| `400`, `404` | «Ссылка не найдена» |
| `410` | «Время действия вашей ссылки истекло» |
| `500`, `503` | «Что-то пошло не так» |

View File

@ -0,0 +1,229 @@
# Конфигурация проекта documentations-api-v2
Документ описывает все переменные окружения и способы конфигурирования сервиса **documentation-api-v2** (`pdm/documentation-api-v2`) — Go-сервис домена «documentations» (v2), отвечающий за диски, документы, бандлы, data source, страницы, публичные ссылки, workflow-обработку и подписи.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`github.com/kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.New()``envconfig.Process("", cfg)`).
Особенности разбора:
- **Префикса нет** — переменные читаются под своими именами (напр. `API_ADDRESS`, `POSTGRES_ADDRESS`), имя задаётся тегом `envconfig:"..."`.
- **Вложенность не используется** — конфиг плоский. Параметры БД вынесены в встроенную структуру `gopg.Config` (`pkg/postgres/gopg/postgres.go`), но остаются на верхнем уровне переменных.
- **Дефолты** заданы тегом `default:"..."` только у части полей (см. таблицы). Поле без дефолта, которое не передали, получает нулевое значение Go (`""`, `0`, `false`) — жёсткой валидации «обязательности» у envconfig в этом коде нет, сервис стартует и с пустыми значениями.
- Переменная БД-сертификата имеет имя с дефисами `YC-PG-CERTIFICATE` (тег `envconfig:"YC-PG-CERTIFICATE"`).
Отдельного конфиг-файла (yaml/toml) у приложения нет. Единственный внешний файл — JSON-описание workflow-задач (`WORKFLOWS_CONFIG_FILEPATH`), разбираемый отдельно в `config/workflows.go` (`NewTasksExecutionConfigFromFilepath`); при ошибке чтения/парсинга сервис **паникует**.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. `.env.template` — только шаблон; приложение **не загружает `.env` автоматически** (в коде нет dotenv) |
| Локально (docker-compose) | `make docker-compose``.docker/docker-compose.yml` с `env_file: .docker/.docker.env` |
| Kubernetes (Helm) | `.helm/values.yaml`, чарт-зависимость `universal-chart`: блоки `envs` (обычные значения) и `secretEnvs` (из k8s-секретов) сервиса `api` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) — переключение окружения/namespace, `HELM_SET_ARGS` |
Способы запуска процессов (`cmd/`):
| Бинарник | Точка входа | Назначение |
| --- | --- | --- |
| `api_server` | `cmd/api_server/main.go` | Основной HTTP API (Fiber). Точка входа контейнера (`CMD ["./api_server"]`) |
| `migrate` (`migrations`) | `cmd/migrate/main.go` | Миграции БД (`go-pg-migrations`): `migrate` / `rollback`. Собирается как `./migrations` |
| `filestream_server` | `cmd/filestream_server/main.go` | Отдельный сервер потоковой отдачи файлов (в основном Dockerfile **не собирается**) |
Сборка образа (`.docker/api.Dockerfile`, тег `-tags migrate`) кладёт `api_server`, `migrations` и `.example.tasks_execution_config.json`. Миграции применяются автоматически при старте приложения (см. README), либо вручную через бинарник `migrations`.
## Переменные приложения
Дефолт `—` означает, что значение в коде по умолчанию не задано (используется нулевое значение Go, если переменную не передать).
### App / окружение
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `APP_NAME` | string | `documentations-backend` | Имя приложения (Sentry `ServerName`) |
| `APP_VERSION` | string | `v1` | Версия (Sentry `Release`) |
| `ENVIRONMENT` | string | — | Окружение: `stage`/`preprod`/`production` (Sentry `Environment`) |
| `NAMESPACE` | string | — | k8s namespace, используется в логике дисков |
### HTTP-сервер
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `API_ADDRESS` | string | — | Адрес прослушивания Fiber. Читается **обоими** бинарниками (`api_server` и `filestream_server`) |
> `api_server` устанавливает большой `BodyLimit` (5 ТБ) и `ReadBufferSize` 96×4096; `filestream_server``BodyLimit` 64 МБ. Оба отдают `GET /ping` для проб k8s; `api_server` дополнительно отдаёт `GET /swagger/*`.
### Database (`gopg.Config`, `pkg/postgres/gopg/postgres.go`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL |
| `POSTGRES_PORT` | string | — | Порт PostgreSQL |
| `POSTGRES_USER` | string | — | Пользователь БД |
| `POSTGRES_DB` | string | — | Имя базы данных |
| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД |
| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений |
| `ENABLE_SSL` | bool | — | Подключение к БД по TLS (сертификат из `YC-PG-CERTIFICATE`) |
| `ENABLE_SQL_QUERY` | bool | — | Логирование SQL-запросов |
| `YC-PG-CERTIFICATE` | string (PEM) | — | CA-сертификат PostgreSQL. Обязателен при `ENABLE_SSL=1` |
### Аутентификация и публичные ссылки
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PUBLIC_KEY` | string (PEM) | — | Публичный RSA-ключ (PKIX) для проверки JWT sarex-backend |
| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | string | — | HMAC-секрет JWT для публичных/временных ссылок на документы |
| `DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES` | uint8 | — | TTL публичной ссылки, минуты |
| `PUBLIC_LINK_HOST` | string | — | Базовый хост генерируемых публичных ссылок |
### Django / IAM (`pkg/django`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_HOST` | string | — | Базовый URL Django/монолита (пользователи, компании, сервис-аккаунты) |
| `DJANGO_BASIC_AUTH` | string (base64) | — | Basic-auth `base64(login:password)` для Django и клиента flows |
| `DJANGO_ORIGINATOR` | string | — | Идентификатор источника (`docs_stage`/`docs_preprod`/`docs_prod`) |
### Внешние сервисы (базовые URL клиентов)
Каждый клиент (`pkg/clients/*`) создаётся с `SetBaseURL(<URL>)` и ретраями. Подробнее по путям — см. `api-v2.ENDPOINTS.md`.
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `DOCUMENTATION_URL` | string | Self-URL сервиса, подставляется в workflow-задачи |
| `WORKFLOW_URL` | string | Сервис workflows (запуск обработки) |
| `WORKSPACE_URL` | string | Сервис workspaces |
| `WORKSPACE_V2_EXTERNAL_URL` | string | Внешний URL workspaces v2 |
| `WORKSPACE_BUNDLE_VERSION` | string | Версия бандла для интеграции с workspaces |
| `MARKS_PROCESSING_URL` | string | Сервис PDF-маркировок (marks) |
| `BIM_API_URL` | string | BIM API v1 |
| `BIM_API_V2_URL` | string | BIM API v2 (bim-core) |
| `BIM_API_URL_EXTERNAL` | string | Внешний URL BIM API |
| `SYSTEM_LOG_URL` | string | Сервис журналирования (system-log) |
| `FLOWS_URL` | string | Сервис flows |
| `FILE_URL_EXTERNAL` | string | Внешний URL для отдачи файлов |
### S3 / MinIO (`pkg/s3/minio`)
Клиент инициализируется только при `ENABLE_S3=1` (иначе `api_server` работает без S3).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_S3` | bool | — | Включить инициализацию S3-клиента |
| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта Yandex S3 |
| `S3_SERVICE_ACCOUNT_STR` | string | — | Альтернатива: JSON сервис-аккаунта строкой |
### BIM-логика
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_BIMV1_FOR_BIMV2` | bool | — | Использовать BIM v1 API вместо v2 в пайплайне документов |
| `LAST_MASTER_BIM` | int | — | Граничный id для маршрутизации BIM master |
| `LAST_SLAVE_1_BIM` | int | — | Граничный id для маршрутизации BIM slave 1 |
### Кеш и файловый стример
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `READ_WRITE_TIMEOUT_FILE_STREAM` | duration | — | Таймаут чтения/записи файлового стримера (напр. `6h`) |
| `CACHE_DEFAULT_EXPIRATION` | duration | — | TTL кеша (напр. `60s`) |
| `CACHE_CLEANUP_INTERVAL` | duration | — | Интервал очистки кеша |
| `USE_CACHE_IN_FILE_STREAMER` | bool | — | Включить кеш в файловом стримере |
### Workflow-задачи
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOWS_CONFIG_FILEPATH` | string | `.example.tasks_execution_config.json` | Путь к JSON-описанию задач (`config/workflows.go`); при ошибке — паника |
| `WORKFLOWS_IMAGES_VERSION` | string | — | Тег образов задач (`develop`/`master`) |
| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов задач |
| `IS_CONVERTED_PDF_UPLOADING_TO_S3` | bool | `true` | Загружать сконвертированный PDF в S3 |
### Наблюдаемость: Sentry и OpenTelemetry
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SENTRY_DSN` | string | — | DSN Sentry |
| `SENTRY_DEBUG` | bool | — | Debug-режим Sentry |
| `ENABLE_OBSERVABILITY` | bool | — | Включить slog-хендлер observability и трассировку запросов БД |
| `OBSERVABILITY_COLLECTOR_ENDPOINT` | string | — | Endpoint OTLP-коллектора логов |
| `TRACER_USE` | bool | `false` | Включить OTEL-трейсинг Fiber |
| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора трейсов |
| `TRACER_USE_INSECURE` | bool | `true` | Подключение к коллектору без TLS |
| `SERVICE_NAME` | string | `documentations-api-v2` | Имя сервиса в трейсах |
| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя OTEL-логгера |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `API_FILESTREAM_ADDRESS` | `.env.template`, `.docker.env` | Присутствует в шаблонах, но **кодом не читается** (см. Замечания) |
| `POSTGRES_EXTERNAL_PORT` | `.docker/docker-compose.yml` | Внешний порт проброса контейнера Postgres |
| `API_VERSION` | `.docker/docker-compose.yml` | Тег образа `api` (по умолчанию `local`) |
| `GITLAB_CREDENTIALS` | `.docker/api.Dockerfile` (build-arg) | Доступ к приватному GitLab при `go build` |
| `CI_COMMIT_SHORT_SHA` | `.docker/api.Dockerfile` (build-arg), CI | Идентификатор сборки |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт — обёртка над зависимостью `universal-chart` (`oci://.../charts`, версия `0.1.7`). Сервис `api`: `deployment` (реплики, ресурсы, пробы `GET /ping:8080`), `service` (ClusterIP `80 → 8080`), `image`, `volumes`.
Смонтированные тома:
- Секрет `documentations-yc-s3``/etc/sarex/yc-s3-storage` (на него указывает `S3_SERVICE_ACCOUNT`);
- ConfigMap `tasks-execution-config-documentation-v2``/etc/app/tasks_execution_config.json` (на него указывает `WORKFLOWS_CONFIG_FILEPATH` в k8s).
Обычные значения (`envs`) задают те же переменные `APP_NAME`, `API_ADDRESS` (`0.0.0.0:8080`), `ENVIRONMENT`, `NAMESPACE`, URL внешних сервисов, `ENABLE_SSL=1`, `ENABLE_S3=1`, флаги трассировки и т.п. — с разбивкой по окружениям (`_default`/`stage`/`preprod`/`production`).
Значения из секретов (`secretEnvs`, монтируются через `secretKeyRef`):
| Переменная | Секрет (`_default`) | Секрет (`preprod`) | Ключ |
| --- | --- | --- | --- |
| `POSTGRES_USER` | `documentations-postgresql-secret` | `ya-pg-secret` | `username` |
| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `ya-pg-secret` | `host` |
| `POSTGRES_PORT` | `documentations-postgresql-secret` | `ya-pg-secret` | `port` |
| `POSTGRES_DB` | `documentations-postgresql-secret` | `ya-pg-secret` | `database` |
| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `ya-pg-secret` | `password` |
| `YC-PG-CERTIFICATE` | `documentations-postgresql-secret` (`ca.crt`) | `yc-pg-certificate` (`certificate`) | см. столбцы |
| `DJANGO_BASIC_AUTH` | `django-auth` | — | `key` |
| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | `yc-jwt-secret` | — | `secret` |
| `PUBLIC_KEY` | `public-key` | — | `key` |
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | Chart version |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` |
| ветка `master` | `preprod` | `documentations-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `0.0.1-prod` |
| `merge_request_event` | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Ключевые переменные: `SERVICE_NAME=documentations-v2`, `RELEASE_NAME`/`CHART_NAME=documentations-v2`, `DOCKERFILE_PATH=.docker/api.Dockerfile`, `HELM_SET_ARGS` (`--set universal-chart.global.env=…`, `--set universal-chart.services.api.image.name.<env>=${IMAGE_NAME}`), флаги `ENABLE_LINTER`/`ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_DEPLOY`.
## Замечания и потенциальные проблемы
- **Нет обязательности полей.** `envconfig` в этом коде не помечает поля как required — при отсутствии переменной берётся нулевое значение Go. Пустые критичные значения (адрес БД, `PUBLIC_KEY`) приведут к ошибке уже в рантайме (падение при `db.Ping`, отказ проверки JWT), а не на этапе разбора конфига.
- **`API_FILESTREAM_ADDRESS` не читается.** Оба сервера слушают `API_ADDRESS`; отдельной переменной для порта файлового стримера в коде нет.
- **Опечатка `POSTGRES_POLL_SIZE`.** В `.env.template`/`.docker.env` встречается `POSTGRES_POLL_SIZE`; код читает `POSTGRES_POOL_SIZE` (в helm имя корректное). Значение с опечаткой не подхватывается.
- **`YC-PG-CERTIFICATE`** — имя с дефисами, читается через явный тег `envconfig`. В `.helm` для preprod монтируется из отдельного секрета `yc-pg-certificate` (ключ `certificate`).
- **Файл workflow-задач обязателен по существу.** Если файл по пути `WORKFLOWS_CONFIG_FILEPATH` отсутствует или не парсится — `config.NewTasksExecutionConfigFromFilepath` вызывает `panic`. Локально нужен `.example.tasks_execution_config.json`, в k8s — том ConfigMap.
- **S3 опционален.** При `ENABLE_S3=0` S3-клиент не создаётся; хендлеры, работающие с хранилищем, будут получать `nil`-хранилище.
- **Автомиграции при старте.** Приложение накатывает миграции автоматически (см. README); ручной прогон — бинарником `migrations migrate`/`migrations rollback`.
## Минимальный набор для локального запуска
Postgres поднимается через `make docker-compose` (образ `timescale/timescaledb-ha:pg13`, инициализация расширений uuid/ltree из `.docker/install-uuid-ltree.sql`). Приложение — бинарник `./api_server`. Минимально задать:
- `API_ADDRESS` (напр. `localhost:8000`);
- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_PASSWORD`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL=0`, `ENABLE_SQL_QUERY`;
- `PUBLIC_KEY` (для проверки JWT sarex-backend), `DOCUMENT_PUBLIC_LINK_JWT_SECRET`;
- `WORKFLOWS_CONFIG_FILEPATH` с существующим JSON (по умолчанию `.example.tasks_execution_config.json`);
- `ENABLE_S3=0`, `TRACER_USE=false`, `ENABLE_OBSERVABILITY=0` — чтобы не поднимать S3/OTEL локально;
- URL внешних сервисов (`DJANGO_HOST`, `WORKFLOW_URL`, `WORKSPACE_URL`, `BIM_API_URL`, `BIM_API_V2_URL`, `MARKS_PROCESSING_URL`, `SYSTEM_LOG_URL`, `FLOWS_URL`) — по мере необходимости для соответствующих сценариев.
Готовые значения-примеры приведены в `api-v2.env.example` (с учётом замечаний выше).

View File

@ -0,0 +1,88 @@
# Эндпоинты, с которыми взаимодействует documentations-api-v2
Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается Go-сервис **documentation-api-v2** (`pdm/documentation-api-v2`).
## Как устроено взаимодействие
Внешние вызовы выполняются типизированными клиентами в `pkg/clients/*` и `pkg/django`. Каждый клиент строится на [`github.com/go-resty/resty/v2`](https://github.com/go-resty/resty) с `SetBaseURL(<URL>)`, ретраями и (опционально) OTEL-трассировкой. Базовый URL берётся из переменной окружения (см. `api-v2.CONFIGURATION.md`); итоговый URL = `<базовый URL>` + путь, указанный в коде метода клиента.
Клиенты создаются в `internal/api/httpserver/server.go` (`App.Run`) и передаются в репозитории/юзкейсы. Аутентификация к внешним сервисам:
- **Django** и **flows** используют Basic-auth из `DJANGO_BASIC_AUTH`;
- **workspace** (`Archive`) пробрасывает пользовательский Bearer-токен (`SetAuthToken`);
- прочие внутренние сервисы вызываются по кластерным адресам без явной авторизации на уровне клиента.
## Базовые адреса по сервисам
| Клиент (`pkg/...`) | Переменная базового URL | Назначение |
| --- | --- | --- |
| `django` | `DJANGO_HOST` | Монолит/IAM: пользователи, компании, сервис-аккаунты, настройки |
| `clients/workspace` | `WORKSPACE_URL` | Сервис рабочих областей |
| `clients/workflows` | `WORKFLOW_URL` | Запуск workflow-обработки |
| `clients/bimv1` | `BIM_API_URL` | BIM API v1 |
| `clients/bimv2` | `BIM_API_V2_URL` | BIM API v2 (bim-core) |
| `clients/flows` | `FLOWS_URL` | Сервис flows (процессы) |
| `clients/marks` | `MARKS_PROCESSING_URL` | Сервис PDF-маркировок |
| `clients/system_log` | `SYSTEM_LOG_URL` | Сервис журналирования |
## Эндпоинты по сервисам
### `django` — монолит/IAM (`pkg/django`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `api/client/settings/` | Клиентские настройки |
| GET | `/api/core/companies/` | Список компаний |
| GET | `/api/core/users/` | Список пользователей |
| GET | `api/core/users/{user_id}/introspect` | Интроспекция пользователя |
| GET | `/api/core/service_accounts/` | Сервис-аккаунты |
| GET | `/api/core/service-accounts/personalized/` | Персонализированные сервис-аккаунты |
### `workspace` — рабочие области (`pkg/clients/workspace`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `internal/v2/workspaces` | Создать рабочую область |
| DELETE | `internal/v2/documents/{document_ids}` | Удалить документы из рабочих областей; возвращает id опустевших областей |
| POST | `api/v1/workspaces/{workspace_id}/archive` | Архивировать рабочую область (с пользовательским Bearer-токеном) |
### `workflows` — workflow-обработка (`pkg/clients/workflows`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `internal/v1/companies/{company_id}/workflows` | Создать/запустить workflow для компании |
### `bimv1` — BIM API v1 (`pkg/clients/bimv1`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/internal/v1/targets/{target_id}/bims-pdm` | Зарегистрировать новый BIM (v1) |
| POST | `/internal/v1/targets/{target_id}/bims-v2-pdm` | Зарегистрировать новый BIM (v2) |
### `bimv2` — BIM API v2 (`pkg/clients/bimv2`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/internal/v1/projects/{project_id}/bims` | Создать BIM в проекте |
### `flows` — процессы (`pkg/clients/flows`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `api/v1/documents/?full=true&document_ids={id}` | Документы процессов по id |
### `marks` — PDF-маркировки (`pkg/clients/marks`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/api/v1/marks/{bundle_id}` | Массовое создание маркировок для бандла |
### `system_log` — журналирование (`pkg/clients/system_log`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/api/v0/system_log` | Отправка пакета записей журнала |
## Обработка ошибок
Клиенты проверяют HTTP-статус ответа и при коде, отличном от ожидаемого (обычно `200`), оборачивают ошибку через `github.com/rotisserie/eris` с указанием имени метода клиента (напр. `bimapi.addNewBim: invalid response code %d expected 200`). Транспортные ошибки resty также оборачиваются `eris.Wrap`. Настроены ретраи (напр. клиент workspace — 5 попыток с паузой 1 c, таймаут 10 c).

View File

@ -0,0 +1,87 @@
# =============================================================================
# documentations-api-v2 (documentation-api-v2) — пример переменных окружения
# Разбор: github.com/kelseyhightower/envconfig, config/config.go + pkg/postgres/gopg/postgres.go
# Префикса у переменных нет; вложенность не используется. Дефолт указан там, где он задан в коде.
# =============================================================================
# --- App ---------------------------------------------------------------------
APP_NAME=documentations-backend # default: documentations-backend
APP_VERSION=v1 # default: v1; используется как Sentry Release
ENVIRONMENT=stage # stage/preprod/production; идёт в Sentry Environment
NAMESPACE=documentations # k8s namespace, используется в бизнес-логике дисков
# --- HTTP-сервер -------------------------------------------------------------
API_ADDRESS=localhost:8000 # адрес прослушивания Fiber (и api_server, и filestream_server)
# API_FILESTREAM_ADDRESS=localhost:8050 # присутствует в шаблоне, но КОДОМ НЕ ЧИТАЕТСЯ (см. Замечания)
# --- PostgreSQL (pkg/postgres/gopg) ------------------------------------------
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_POOL_SIZE=1 # в helm: POSTGRES_POOL_SIZE (в шаблоне встречается опечатка POSTGRES_POLL_SIZE — не читается)
ENABLE_SSL=0 # 1 → TLS к БД по сертификату из YC-PG-CERTIFICATE
ENABLE_SQL_QUERY=1 # логирование SQL-запросов
YC-PG-CERTIFICATE="" # PEM CA-сертификат PostgreSQL (имя с дефисами, читается через envconfig-тег)
# POSTGRES_EXTERNAL_PORT=6432 # только для docker-compose, приложением не читается
# --- Django / IAM (pkg/django) -----------------------------------------------
DJANGO_HOST=https://stage.sarex.io
DJANGO_BASIC_AUTH= # base64(login:password) для Basic-auth в Django и flows
DJANGO_ORIGINATOR=docs_stage # идентификатор источника для system-log/Django
# --- Внешние сервисы (base URL клиентов) -------------------------------------
DOCUMENTATION_URL=http://api-v2-service.documentations/ # self-URL, подставляется в workflow-задачи
WORKFLOW_URL=http://workflows-api-service.platform:8000/
WORKSPACE_URL=http://workspaces-backend-service.platform:8000/
WORKSPACE_V2_EXTERNAL_URL=https://stage.sarex.io/workspaces-v2/
WORKSPACE_BUNDLE_VERSION=v1
MARKS_PROCESSING_URL=http://marks-service.documentations:8000
BIM_API_URL=http://bim-api-service.bim-api-stage/
BIM_API_V2_URL=http://bim-core-api.platform.svc.cluster.local:8000/
BIM_API_URL_EXTERNAL=https://stage-api.sarex.io/bim
SYSTEM_LOG_URL=http://system-log-api-service.platform:80
FLOWS_URL=
FILE_URL_EXTERNAL= # внешний URL для отдачи файлов
# --- S3 / MinIO (pkg/s3/minio) -----------------------------------------------
ENABLE_S3=0 # 1 → инициализировать S3-клиент
S3_SERVICE_ACCOUNT=/etc/sarex/yc-s3-storage/yc-s3-service-account.json # путь к JSON сервис-аккаунта
S3_SERVICE_ACCOUNT_STR= # альтернатива: JSON сервис-аккаунта строкой
# --- Публичные ссылки на документы -------------------------------------------
PUBLIC_LINK_HOST= # хост генерируемых публичных ссылок
DOCUMENT_PUBLIC_LINK_JWT_SECRET="" # HMAC-секрет JWT публичной ссылки
DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES=5 # TTL публичной ссылки (uint8)
# --- Аутентификация ----------------------------------------------------------
PUBLIC_KEY="" # PKIX RSA public key (PEM) для проверки JWT sarex-backend
# --- BIM-логика --------------------------------------------------------------
USE_BIMV1_FOR_BIMV2=0 # использовать BIM v1 API вместо v2
LAST_MASTER_BIM=0 # граничные id для маршрутизации BIM-запросов
LAST_SLAVE_1_BIM=0
# --- Кеш / файловый стример --------------------------------------------------
READ_WRITE_TIMEOUT_FILE_STREAM=6h # Go duration
CACHE_DEFAULT_EXPIRATION=60s # Go duration
CACHE_CLEANUP_INTERVAL=60s # Go duration
USE_CACHE_IN_FILE_STREAMER=1 # bool
# --- Workflow-задачи ---------------------------------------------------------
WORKFLOWS_IMAGES_VERSION=develop # тег образов задач (develop/master)
WORKFLOWS_CONFIG_FILEPATH=.example.tasks_execution_config.json # default; в k8s — /etc/app/tasks_execution_config.json
CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q # default; реестр образов задач
IS_CONVERTED_PDF_UPLOADING_TO_S3=true # default: true
# --- Наблюдаемость: Sentry + OpenTelemetry -----------------------------------
SENTRY_DSN=""
SENTRY_DEBUG=false
ENABLE_OBSERVABILITY=0 # bool; включает slog-хендлер и трассировку в БД
OBSERVABILITY_COLLECTOR_ENDPOINT=0 # endpoint OTLP-коллектора логов
TRACER_USE=false # default: false; включить OTEL-трейсинг Fiber
TRACER_HOST=localhost:4317 # default: localhost:4317
TRACER_USE_INSECURE=true # default: true
SERVICE_NAME=documentations-api-v2 # default: documentations-api-v2
TRACER_LOGGER_NAME=tracer_logger # default: tracer_logger

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,345 @@
# Конфигурация проекта documentations-api
Документ описывает все переменные окружения и способы конфигурирования сервиса документаций (`documentation-api`). Репозиторий собирает **два бинарника/образа**, разворачиваемых в неймспейсе `documentations`:
| Бинарник | Точка входа | Образ | Deployment | Назначение |
| --- | --- | --- | --- | --- |
| **API** | `cmd/api` | `documentations` (`cr.yandex/.../documentations-api`) | `documentations-api` | Основной REST-API: диски, документы, бандлы, права, воркспейсы, штампы, публичные ссылки и т. д. |
| **Filestream** | `cmd/filestreamer` | `documentations-api-files` (`.../documentations-filestream`) | `documentations-filestream` | Потоковая отдача/приём файлов из S3 (скачивание документов и бандлов, догрузка частей, gzip/range). |
Оба бинарника используют **одну и ту же структуру конфигурации** (`config.Config`) — различия только в том, какие поля реально задействуются (см. раздел «Различия api и filestream»).
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`github.com/kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.FromEnv()``envconfig.Process("", &cfg)`).
Особенности разбора:
- **Префикса нет**`envconfig.Process` передаётся пустая строка) — имена переменных плоские, задаются тегом `envconfig:"..."` у каждого поля структуры `Config`;
- **Вложенности нет** — двойных подчёркиваний/секций, как в pydantic, здесь не используется;
- **Значения по умолчанию** задаются тегом `default:"..."` прямо в структуре (например `USE_BIM_INSERTER default:"true"`). Поля без `default` и без значения в окружении получают нулевое значение типа (`""`, `0`, `false`, `nil`) — то есть формально **обязательных полей с ошибкой старта у envconfig нет**; отсутствующая переменная просто становится «пустой», а несостоятельность конфигурации всплывает позже в рантайме (например, невозможность подключиться к БД/S3);
- **Отдельного конфиг-файла (yaml/toml) у приложения нет.** Приложение **не загружает `.env` автоматически** — переменные должны быть в окружении процесса (в контейнере их проставляет Helm, локально — вручную или через `docker-compose --env-file`);
- Единственный внешний файл конфигурации — `WORKFLOWS_CONFIG_FILEPATH` (JSON с параметрами запуска задач обработки, см. `.example.tasks_execution_config.json`), читается `workflow.NewTasksExecutionConfigFromFilepath` при старте api.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. Готовый шаблон — `.docker/.env` |
| Локально (контейнеры) | `.docker/docker-compose.yml` + `make docker`: значения из `.docker/.env` и `.docker/.docker.env` |
| Kubernetes (Helm) | `.helm/values.yaml` (universal-chart): блоки `services.api.envs`/`secretEnvs` и `services.filestream.envs`/`secretEnvs` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: `workflow.rules` (окружение по ветке/тегу) и `HELM_SET_ARGS` |
Способы запуска процессов:
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `api` (`make api` / `make run-api-dev`) | `cmd/api` | Основной REST-API |
| `filestreamer` (`make file_api` / `make run-filestreamer-dev`) | `cmd/filestreamer` | Файловый стример |
| `migrations migrate` | `cmd/migrations` | Прогон миграций БД |
| `delete_expired_public_links` | `cmd/scripts/delete_expired_public_links` | CronJob удаления просроченных публичных ссылок |
| `refresh_latest_bundle_filters_view` | `cmd/scripts/refresh_latest_bundle_filters_view` | CronJob обновления материализованного представления |
| `cleanup_failed_s32d_sessions` | `cmd/scripts/cleanup_failed_s32d_sessions` | CronJob очистки зависших s3d→ifc сессий |
Порядок запуска в контейнере: сначала миграции, затем сервер.
- `entrypoint.sh` (api): `migrations migrate``api`;
- `file_entrypoint.sh` (filestream): `migrations migrate``filestreamer`.
Для локальной разработки предусмотрен hot-reload через `air`: `.air.toml` (api, `./tmp/api`) и `.air.filestreamer.toml` (filestream, `./tmp/filestreamer`). Окружение сборки описано в `flake.nix` (Go 1.22 + `air`), образы собираются с Go 1.24 (`.docker/api.dockerfile`, `.docker/api-filestream.dockerfile`).
## HTTP-фреймворк, порты, health
- Роутер — `gorilla/mux`, обёрнутый в `rest.NewCustomRouter` (`gitlab.sarex.io/platform/gotools/rest`). Ответы оборачиваются в JSON (`rest.JSONResponse`), включена gzip-компрессия (`gorilla/handlers.CompressHandler`).
- Пробы `liveness`/`readiness` в Helm ходят на `GET /ping` (эндпоинт предоставляется кастомным роутером `rest`, в коде маршрутов репозитория не объявлен).
- Порт api — из `API_ADDRESS`, порт filestream — из `API_ADDRESS_FILE` (в k8s оба слушают `0.0.0.0:8080`).
- Таймауты: у api `Read/WriteTimeout = 30m` (жёстко в коде), у filestream `Read/WriteTimeout = READ_WRITE_TIMEOUT_FILE_STREAM` (по умолчанию окружения — `6h`).
- Оба сервиса регистрируют `net/http/pprof` (`/debug/pprof/...`).
## Аутентификация и авторизация
Разбор описан в `cmd/api/bootstrap.go`, `cmd/filestreamer/main.go`, `pkg/midleware/auth.go`, `pkg/midleware/signature.go`.
Цепочка middleware для `/api/v1/*`: `sentry``JSONResponse``reqid``logging` → (**только filestream**: `SignatureMiddleware`) → `auth.JWTToCtx``JWTUserExtractorFromCtx``DjangoToCtx``NewAuthMiddleware`/`NewAuthMiddlewareWithZitadel` → `DeleteJWTFromQueryMiddleware``sentry.AddUser`.
- **JWT**: токен из заголовка `Authorization: Bearer ...` проверяется по RSA-публичному ключу (`PUBLIC_KEY`, формат PEM/PKIX). Из claims извлекаются `company_ids` и `service_accounts`.
- **Zitadel** (опционально, `USE_ZITADEL=1`): дополнительная проверка токена через Zitadel и разбор метаданных пользователя (`urn:zitadel:iam:user:metadata`, base64-поля `company_ids`/`service_accounts`).
- **Identity-заголовок**: при наличии `Identity` метаданные берутся из него.
- **Публичные ссылки/временные загрузки**: пути `/api/v1/public/...`, `/api/v1/public_link_mrpas/...` и запросы с `download_type=temporary` проверяются по HMAC-секрету `DOCUMENT_PUBLIC_LINK_JWT_SECRET`.
- **Подписанные ссылки (только filestream)**: при наличии query-параметра `signature` `SignatureMiddleware` убирает `Authorization` и проверяет подпись (`SIGNATURE_SECRET_KEY`) с `expires_at`; включается флагом `ENABLE_SIGNATURE_IN_URL` (при `true` обязателен рабочий Valkey — иначе api/filestream завершается с кодом 2).
- Маршруты `/internal/v1/*` используют облегчённую цепочку (`auth.JWTToCtxIfPossible`) без обязательной проверки.
## Переменные приложения
В столбце «Переменная» — точное имя (тег `envconfig`). Дефолт `—` означает, что тег `default` не задан (поле получает нулевое значение типа, если переменная не задана в окружении).
### PostgreSQL
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL |
| `POSTGRES_PORT` | string | — | Порт PostgreSQL |
| `POSTGRES_USER` | string | — | Пользователь БД |
| `POSTGRES_PASSWORD` | string | — | Пароль БД |
| `POSTGRES_DB` | string | — | Имя базы данных |
| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (**только api**; filestreamer использует фиксированное значение 50) |
| `ENABLE_SSL` | bool | — | TLS-подключение к БД; при `true` используется `YC-PG-CERTIFICATE` |
| `YC-PG-CERTIFICATE` | string | — | PEM CA-сертификат PostgreSQL (имя с дефисами; в проде — из секрета `yc-pg-certificate`) |
### S3
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_S3` | bool | — | Включить S3. В api при `false` сервис стартует без S3-клиента; в filestream S3 нужен всегда |
| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта S3 (монтируется как файл) |
| `S3_SERVICE_ACCOUNT_STR` | string | — | Альтернатива: JSON сервис-аккаунта строкой |
### API-адреса
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `API_ADDRESS` | string | — | Адрес прослушивания основного API (`cmd/api`) |
| `API_ADDRESS_FILE` | string | — | Адрес прослушивания файлового стримера (`cmd/filestreamer`) |
### Sarex backend (Django) и Zitadel
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_HOST` | string | — | Базовый URL Sarex backend (Django). Используется клиентами `django`, `users`, `sarex_backend`, `accounts` |
| `DJANGO_BASIC_AUTH` | string | — | Basic-auth для системных вызовов Django |
| `DJANGO_BASIC_AUTH_FOR_GET_USER` | string | — | Отдельный basic-auth для запросов пользователей |
| `DJANGO_ORIGINATOR` | string | — | Идентификатор источника запросов |
| `USE_ZITADEL` | bool | — | Включить проверку токенов через Zitadel |
| `ZITADEL_DOMAIN` | string | — | Домен Zitadel (IdP) |
| `ZITADEL_ACCOUNT` | string | — | Путь к JSON сервис-аккаунта Zitadel |
### Внешние сервисы (базовые URL)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `FILE_URL_EXTERNAL` | string | — | Внешний URL файлового сервиса |
| `DOCUMENTATION_URL` | string | — | URL самого сервиса документаций (для внутренних ссылок) |
| `WORKFLOW_URL` | string | — | URL сервиса workflows (создание/чтение процессов обработки) |
| `WORKSPACE_URL` | string | — | URL сервиса воркспейсов |
| `WORKSPACE_V2_EXTERNAL_URL` | string | — | Внешний URL воркспейсов v2 |
| `WORKSPACE_BUNDLE_VERSION` | string | — | Версия бандла воркспейса (`v1`) |
| `MARKS_PROCESSING_URL` | string | — | URL сервиса штампов/маркировок (при HTTP-режиме) |
| `BIM_API_URL` | string | — | URL BIM-API v1 |
| `BIM_API_V2_URL` | string | — | URL BIM-API v2 (bim-core-api) |
| `BIM_API_URL_EXTERNAL` | string | — | Внешний URL BIM-API |
| `SYSTEM_LOG_URL` | string | — | URL сервиса системного лога |
| `FLOWS_URL` | string | — | URL сервиса flows |
| `AUTOMATION_URL` | string | — | URL сервиса автоматизаций |
| `TRANSMITTALS_BASE_URL` | string (nullable) | `nil` | URL сервиса трансмитталов; клиент создаётся только если переменная задана |
### Публичные ссылки и JWT
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PUBLIC_LINK_HOST` | string | — | Хост публичных ссылок на документы |
| `PUBLIC_KEY` | string | — | RSA-публичный ключ (PEM/PKIX) для проверки JWT |
| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | string | — | HMAC-секрет для JWT публичных ссылок и временных загрузок |
| `DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES` | uint8 | — | Время жизни JWT публичной ссылки (мин.) |
| `PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED` | bool | `true` | Коннектор публичных ссылок для папок |
### Подпись ссылок (signature-in-URL)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_SIGNATURE_IN_URL` | bool | `false` | Проверять подпись в URL. При `true` требуется рабочий Valkey (иначе сервис завершается с кодом 2) |
| `SIGNATURE_SECRET_KEY` | string | `""` | Секрет для подписи ссылок скачивания |
| `SIGNATURE_IN_URL_EXPIRATION_SECONDS` | uint64 | `600` | Срок жизни подписи (сек.) |
| `ENABLE_AUTH_JWT_IN_URL` | bool | `true` | Добавлять `auth_jwt` в URL |
### Valkey (Redis-совместимый) — кэши
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `VALKEY_ADDR` | string | `localhost:6380` | Адрес `host:port`. Пустая строка полностью отключает клиент Valkey |
| `VALKEY_LOGIN` | string | `""` | Логин |
| `VALKEY_HOST` | string | `""` | Хост (доп. поле) |
| `VALKEY_PASSWORD` | string | `""` | Пароль |
| `VALKEY_DB` | int | `0` | Номер БД Redis/Valkey |
| `VALKEY_CACHE_TTL` | duration | `1h` | TTL кэша |
| `VALKEY_SSL` | bool | `false` | TLS-подключение |
| `VALKEY_SSL_CA_CERTS` | string | `""` | CA-сертификат для TLS |
### Файловый стример и in-memory кэш
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `READ_WRITE_TIMEOUT_FILE_STREAM` | duration | — | Read/Write-таймаут HTTP-сервера filestream (в окружении — `6h`) |
| `USE_CACHE_IN_FILE_STREAMER` | bool | — | Включить in-memory кэш в filestream-хранилище |
| `CACHE_DEFAULT_EXPIRATION` | duration | — | TTL записей кэша |
| `CACHE_CLEANUP_INTERVAL` | duration | — | Интервал очистки кэша |
### BIM / обработка файлов
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_BIMV1_FOR_BIMV2` | bool | — | Использовать BIM v1 для v2 |
| `USE_BIM_INSERTER` | bool | `true` | Включить BIM-inserter |
| `USE_LEGACY_BIM_FLOW` | bool | `false` | Старый flow BIM |
| `LAST_MASTER_BIM` | uint64 | — | Граница master-BIM |
| `LAST_SLAVE_1_BIM` | uint64 | — | Граница slave-1-BIM |
| `LAST_SLAVE_2_BIM` | uint64 | — | Граница slave-2-BIM |
| `CONVERT_DWG_TO_GEOJSON` | bool | `true` | Конвертация DWG→GeoJSON |
| `CONVERT_DXF_TO_GEOJSON` | bool | `true` | Конвертация DXF→GeoJSON |
| `IS_CONVERTED_PDF_UPLOADING_TO_S3` | bool | `true` | Загружать сконвертированный PDF в S3 |
| `DELETE_S3D_AFTER_MESHOPT` | bool | `false` | Удалять s3d после mesh-оптимизации |
| `WORKFLOW_IMAGES_VERSION` | string | — | Тег образов workflow |
| `WORKFLOWS_IMAGES_VERSION` | string | — | Тег образов задач обработки (используется клиентом `workflow`) |
| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр контейнеров для образов задач |
| `WORKFLOWS_CONFIG_FILEPATH` | string | `.example.tasks_execution_config.json` | Путь к JSON с ресурсами задач обработки |
### Штампы/маркировки (HTTP или RabbitMQ)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_MARKS_RABBITMQ` | bool | `0` | `0` — ходить в `MARKS_PROCESSING_URL` по HTTP; `1` — через RabbitMQ |
| `MARKS_RABBITMQ_HOST` | string | `""` | Хост RabbitMQ |
| `MARKS_RABBITMQ_PORT` | string | `""` | Порт RabbitMQ |
| `MARKS_RABBITMQ_USER` | string | `""` | Пользователь (из секрета) |
| `MARKS_RABBITMQ_PASSWORD` | string | `""` | Пароль (из секрета) |
| `MARKS_RABBITMQ_API` | string | `""` | Vhost/имя очереди |
### Rate limit эндпоинта метаданных
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `METADATA_RATE_LIMIT_ENABLED` | bool | `true` | Включить rate-limit для `/documents/metadata` |
| `METADATA_RATE_LIMIT_MAX_REQUESTS` | int | `10` | Макс. число запросов |
### Почта
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_MAILGUN` | bool | `true` | Флаг использования Mailgun |
| `ENABLE_SMTP` | bool | `false` | Флаг использования SMTP |
### Наблюдаемость
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENVIRONMENT` | string | — | Окружение (для Sentry/трейсинга) |
| `SENTRY_DSN` | string | — | DSN Sentry |
| `SENTRY_DEBUG` | bool | — | Отладка Sentry |
| `NAMESPACE` | string | — | Неймспейс (для контекста запусков задач) |
| `ENABLE_SQL_QUERY` | bool | — | Логировать SQL-запросы |
| `TRACER_USE` | bool | `false` | Включить OpenTelemetry-трейсинг |
| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора |
| `TRACER_USE_INSECURE` | bool | `true` | Подключение к коллектору без TLS |
| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя логгера трейсинга |
| `SERVICE_NAME` | string | `documentations-api` | Имя сервиса в трейсах (используется api) |
| `SERVICE_NAME_FILESTREAM` | string | `filestream-api` | Имя сервиса в трейсах (используется filestream) |
## Различия api и filestream
Оба процесса читают одну и ту же структуру `config.Config`, но:
| Аспект | api (`cmd/api`) | filestream (`cmd/filestreamer`) |
| --- | --- | --- |
| Слушает адрес из | `API_ADDRESS` | `API_ADDRESS_FILE` |
| Пул соединений к БД | `POSTGRES_POOL_SIZE` | фиксировано `50` (+ `PoolTimeout=1m`), `POSTGRES_POOL_SIZE` игнорируется |
| Read/Write-таймаут сервера | жёстко `30m` | `READ_WRITE_TIMEOUT_FILE_STREAM` |
| S3 | опционален (`ENABLE_S3`) | обязателен (при отсутствии кредов процесс завершается) |
| In-memory кэш хранилища | выключен | управляется `USE_CACHE_IN_FILE_STREAMER` / `CACHE_*` |
| Имя сервиса в трейсах | `SERVICE_NAME` | `SERVICE_NAME_FILESTREAM` |
| Signature-middleware на `/api/v1` | нет | есть (`SignatureMiddleware`) |
| Набор маршрутов | полный REST CRUD (`cmd/api/routes_api.go`, `routes_internal.go`) | только потоковые скачивания/загрузки файлов (`cmd/filestreamer/routes_api.go`, `routes_internal.go`) |
**Что делает filestream-бинарник.** Это отдельный HTTP-сервис для тяжёлой потоковой работы с файлами, вынесенный из основного API, чтобы не блокировать его долгими соединениями (отсюда таймаут в часы и увеличенный пул БД). Публичные маршруты (`/api/v1`):
- `GET /documents/folders` — скачивание нескольких папок архивом (gzip);
- `GET|HEAD|POST /bundles/...` — скачивание файлов бандла; при `?format=gz` отдаётся без повторного сжатия, иначе — `CompressHandler`; поддержаны HEAD (range) и внешний матчер `DownloadMatcherExternal`;
- `GET|HEAD|POST /pages/...` — скачивание страниц (постранично);
- `GET|HEAD /documents/...` — скачивание по документам; `POST /documents/...` — скачивание по списку bundle-id;
- `POST /bundles_mrpas/...` и `GET /public_link_mrpas/...` — выгрузка MRPA (в т. ч. по публичной ссылке).
Внутренние маршруты (`/internal/v1`): скачивание/загрузка бандлов между сервисами и `POST /upload_finish/bundles/...`.
## Переменные сборки и запуска (не читаются кодом приложения)
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `APP_VERSION` | `Makefile`, dockerfiles | Версия, зашиваемая в бинарь (`-ldflags -X main.version`) |
| `CI_COMMIT_SHORT_SHA` | dockerfiles | Тег версии образа files |
| `GITLAB_CREDENTIALS` | dockerfiles (build-arg) | Доступ к приватным Go-модулям `gitlab.sarex.io` |
| `API_VERSION` | `.docker/docker-compose.yml` | Тег локально запускаемого образа |
| `POSTGRES_EXTERNAL_PORT` | `.docker/docker-compose.yml` | Внешний порт локального Postgres |
| `KEY_JWT` / `JWT_KEY` | `.docker/.env`, docker-compose | JWT-ключ **только для интеграционных тестов**, кодом приложения не читается |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт основан на `universal-chart` (зависимость из `Chart.yaml`) и описывает два сервиса — `services.api` и `services.filestream`. У каждого свои блоки `envs` (обычные значения, с разбивкой по окружениям `_default`/`stage`/`preprod`/`production`) и `secretEnvs` (значения из k8s-секретов). Наборы переменных у обоих сервисов практически идентичны.
Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `SIGNATURE_SECRET_KEY` | `documentations-download-secret` | `secret` |
| `VALKEY_ADDR` | `valkey-secret` | `url` |
| `VALKEY_LOGIN` | `valkey-secret` | `login` |
| `VALKEY_PASSWORD` | `valkey-secret` | `password` |
| `VALKEY_HOST` | `valkey-secret` | `host` |
| `VALKEY_PORT` | `valkey-secret` | `port` |
| `VALKEY_CA_CERTS` | `valkey-secret` | `cert` |
| `POSTGRES_USER` | `documentations-postgresql-secret` | `user` |
| `POSTGRES_PORT` | `documentations-postgresql-secret` | `port` |
| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `host` |
| `POSTGRES_DB` | `documentations-postgresql-secret` | `database` |
| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `password` |
| `DJANGO_BASIC_AUTH` | `django-auth` | `key` |
| `DJANGO_BASIC_AUTH_FOR_GET_USER` | `django-auth-get-user` | `key` |
| `DOCUMENT_PUBLIC_LINK_JWT_SECRET` | `yc-jwt-secret` | `secret` |
| `PUBLIC_KEY` | `public-key` | `key` |
| `MARKS_RABBITMQ_USER` | `cde-rabbitmq-secret` (в api; prod — `marks-rabbit-secret`) / `marks-rabbit-secret` (в filestream) | `user` |
| `MARKS_RABBITMQ_PASSWORD` | то же | `password` |
| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `certificate` |
Тома (`volumes`) монтируют секреты как файлы: `documentations-yc-s3``/etc/sarex/yc-s3-storage` (на него указывает `S3_SERVICE_ACCOUNT`), `zitadel-account``/etc/sarex/zitadel` (на него указывает `ZITADEL_ACCOUNT`). Файл `WORKFLOWS_CONFIG_FILEPATH` в проде — `/etc/app/tasks_execution_config.json`.
Ingress включён только у `filestream` (`ingress.enabled: false` по умолчанию, path `/files/api/` → rewrite `/api/`); у api ingress-блока в values нет — сервис доступен через `documentations-api-svc`.
Прочие значения чарта (не переменные приложения): `deployment.*` (реплики stage/preprod/prod = 1/3/6, ресурсы, revisionHistoryLimit), `probes` (`/ping`), `service.*`, `serviceAccount`, `imagePullSecrets: dockerhub`, `affinity` (podAntiAffinity у filestream), а также блок `cronjobs` (`delete_expired_public_links`, `refresh_latest_bundle_filters_view`, `refresh_string_path_materialized_view`).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Окружение переключается по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | universal-chart env |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `documentations` | `stage` |
| ветка `master` | `preprod` | `documentations-preprod` | `preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `production` |
| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Общие для всех окружений: `RELEASE_NAME=documentations`, `CHART_NAME=documentations`, `SERVICE_NAME: documentations`, `DOCKERFILE_PATH: .docker/api.dockerfile`, `IMAGE_PATH: api.deployment.image`. Через `HELM_SET_ARGS` проставляются образы: `services.api.image` (основной), `services.filestream.image` (`IMAGE_NAME_API_FILES`, dockerfile `.docker/api-filestream.dockerfile`), а также образы кронджоб `cronjobs.delete_expired_public_links`, `cronjobs.refresh_latest_bundle_filters_view`, `cronjobs.cleanup_failed_s32d_sessions`. Дополнительные образы собираются отдельными job-ами `build_files`, `build_public_link_autodeletion`, `build_refresh_latest_bundle_filters_view`, `build_cleanup_failed_s32d_sessions` (только на `stage`/`master`/тег).
## Замечания и потенциальные проблемы
- **У envconfig нет «обязательных» полей.** Отсутствующая переменная без `default` становится нулевым значением, ошибка старта не выбрасывается. Некорректная конфигурация проявляется в рантайме (не удаётся подключиться к БД/S3, невалидный `PUBLIC_KEY` при первой проверке JWT и т. п.).
- **`.env` не подхватывается автоматически** — приложение читает только окружение процесса. Локально удобнее запускать через `docker-compose --env-file` (`make docker`) или экспортировать `.docker/.env` вручную.
- **`POSTGRES_POOL_SIZE` игнорируется в filestream** — там пул жёстко задан как `50`. В api берётся из переменной.
- **`YC-PG-CERTIFICATE`** — имя с дефисами (не в стиле `SNAKE_CASE`), но envconfig читает его по точному тегу. Значение приходит из секрета и используется как содержимое PEM (`AppendCertsFromPEM`), а не как путь к файлу.
- **Рассинхрон имён Valkey.** В Helm задаются `VALKEY_PORT` и `VALKEY_CA_CERTS`, но код читает `VALKEY_ADDR` (host:port одной строкой) и `VALKEY_SSL_CA_CERTS`. Переменные `VALKEY_PORT`/`VALKEY_CA_CERTS` приложением напрямую не читаются (адрес и CA берутся из `VALKEY_ADDR`/`VALKEY_SSL_CA_CERTS`).
- **`HOST` и `DOCUMENTATION_EXTERNAL_URL` из Helm кодом не читаются** — в `config.Config` таких полей нет (для внутренних ссылок используется `DOCUMENTATION_URL`, `FILE_URL_EXTERNAL`, `PUBLIC_LINK_HOST`).
- **`ENABLE_SIGNATURE_IN_URL=true` требует Valkey.** Если клиент Valkey не инициализировался, а флаг включён, оба процесса завершаются с кодом `2`.
- **Флаги `ENABLE_MAILGUN`/`ENABLE_SMTP`** присутствуют в конфиге и Helm, но собственной отправкой почты сервис не занимается (в отличие от transmittal-api); это флаги для внешних интеграций.
- **`TRANSMITTALS_BASE_URL` — nullable.** Клиент трансмитталов создаётся только если переменная задана; иначе связанные вызовы пропускаются.
- **Штампы: HTTP vs RabbitMQ.** При `USE_MARKS_RABBITMQ=1` используется RPC-клиент через RabbitMQ (`MARKS_RABBITMQ_*`), при `0` — HTTP-клиент на `MARKS_PROCESSING_URL`. В values для prod-секретов имя `marks-rabbit-secret` отличается от stage/preprod (`cde-rabbitmq-secret`).
## Минимальный набор для локального запуска
Ориентир — `.docker/.env` (+ `make docker` для запуска в контейнерах вместе с Postgres). Минимально нужно задать:
- `API_ADDRESS`, `API_ADDRESS_FILE`;
- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL=0`;
- `ENABLE_S3` (`0` для api без S3; для filestream — `1` и `S3_SERVICE_ACCOUNT`/`S3_SERVICE_ACCOUNT_STR`);
- `DJANGO_HOST`, `DJANGO_ORIGINATOR`, `NAMESPACE`;
- сервисные URL по необходимости: `WORKFLOW_URL`, `WORKSPACE_URL`, `SYSTEM_LOG_URL`, `FLOWS_URL`, `MARKS_PROCESSING_URL`;
- для filestream: `READ_WRITE_TIMEOUT_FILE_STREAM`, `USE_CACHE_IN_FILE_STREAMER`, `CACHE_DEFAULT_EXPIRATION`, `CACHE_CLEANUP_INTERVAL`;
- `ENABLE_SQL_QUERY=1` (для отладки), `TRACER_USE=false`;
- `PUBLIC_KEY`/`DOCUMENT_PUBLIC_LINK_JWT_SECRET` — для реальной проверки JWT (локально можно оставить пустыми, но защищённые ручки будут отклонять токены).
Готовый пример со всеми значениями приведён в `api.env.example` (рядом с этим документом) и в `.docker/.env` исходного репозитория.

View File

@ -0,0 +1,114 @@
# Эндпоинты внешних сервисов, с которыми взаимодействует documentations-api
Документ описывает HTTP-эндпоинты внешних сервисов, к которым обращается сервис документаций (оба бинарника — `cmd/api` и `cmd/filestreamer`). В отличие от фронтенда, единого декларативного реестра эндпоинтов здесь нет — каждый внешний сервис инкапсулирован в собственном клиенте в каталогах `clients/` и `pkg/`.
## Как устроено взаимодействие
Клиенты создаются при старте (`cmd/api/routes_api.go`, `cmd/filestreamer/*`) и используют базовый URL из соответствующей переменной окружения (см. `config/config.go`). Большинство клиентов построены на `go-resty/resty` (метод `SetHostURL`/`SetBaseURL`), часть — на внутренних http-обёртках `gitlab.sarex.io/platform/gotools`. Итоговый URL = `<базовый URL сервиса>` + путь из клиента.
Аутентификация исходящих запросов:
- к Sarex backend (Django) — HTTP Basic (`DJANGO_BASIC_AUTH`, а для получения пользователей — `DJANGO_BASIC_AUTH_FOR_GET_USER`); для части ручек проксируется заголовок `Identity`/`Bearer`;
- к остальным сервисам — по внутренней сети кластера, как правило без внешней авторизации.
## Базовые URL по сервисам
| Сервис | Переменная окружения | Клиент (каталог) |
| --- | --- | --- |
| Sarex backend (Django) | `DJANGO_HOST` | `clients/django`, `pkg/django`, `pkg/users`, `pkg/sarex_backend`, `clients/accounts` |
| Flows | `FLOWS_URL` | `pkg/flows` |
| Workflows | `WORKFLOW_URL` | `clients/workflow`, `pkg/workflows` |
| Workspaces | `WORKSPACE_URL` | `clients/workspace` |
| Transmittals | `TRANSMITTALS_BASE_URL` (опц.) | `pkg/transmittal` |
| Automation | `AUTOMATION_URL` | `pkg/automation` |
| Marks (штампы, HTTP) | `MARKS_PROCESSING_URL` | `pkg/marks/base` |
| Marks (штампы, RabbitMQ) | `MARKS_RABBITMQ_*` | `pkg/marks/rpc` |
| BIM-API v1 | `BIM_API_URL` | `clients/bim-api` |
| BIM-API v2 (bim-core-api) | `BIM_API_V2_URL` | `clients/bim-api-v2` |
| System log | `SYSTEM_LOG_URL` | `pkg/system_log` |
Дополнительно сервис работает с S3 (объектное хранилище, креды из `S3_SERVICE_ACCOUNT`/`S3_SERVICE_ACCOUNT_STR`) и PostgreSQL — это не HTTP-сервисы и в таблицах ниже не приводятся.
## Эндпоинты по сервисам
### Sarex backend (Django) — `DJANGO_HOST`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| GET | `/api/core/users/` | `clients/django/users.go` | Список пользователей |
| GET | `/api/core/users/{id}/` | `clients/django/users.go`, `pkg/users/client.go` | Пользователь по id |
| GET | `/api/core/users/{id}/introspect` | `clients/django/users.go` | Интроспекция пользователя |
| GET | `/api/core/users/{id}` | `pkg/django/client.go` | Пользователь по id (внутренний клиент) |
| GET | `/api/client/settings/` | `clients/django/settings.go` | Клиентские настройки |
| GET | `/api/core/service-accounts/personalized/` | `clients/django/settings.go` | Персонализированные сервисные аккаунты |
| GET | `/api/core/service_accounts/` | `clients/django/service_accounts.go`, `clients/accounts` | Сервисные аккаунты |
| GET | `/api/core/companies/` | `clients/django/companies.go` | Список компаний |
| GET | `/api/core/mrpa/{id}/` | `pkg/sarex_backend/client.go` | MRPA по id (прокидывается заголовок `Identity`) |
### Flows — `FLOWS_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| GET | `internal/v1/documents/?full=true&document_ids={id}` | `pkg/flows/client.go` | Документы в процессах (flows) по id |
| GET/POST | `internal/v1/documents/` | `pkg/flows/client.go` | Документы процессов |
### Workflows — `WORKFLOW_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `internal/v1/companies/{company_id}/workflows` | `clients/workflow/client.go` | Создать workflow обработки (BIM/PDF/DWG/DEM/DOCX и т. д.); образы задач — из `CONTAINER_REGISTRY` + `WORKFLOWS_IMAGES_VERSION` |
| GET | `v1/workflows/{id}` | `pkg/workflows/client.go` | Прочитать workflow по id |
### Workspaces — `WORKSPACE_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `internal/v2/workspaces` | `clients/workspace/client.go` | Создать воркспейс |
| DELETE | `internal/v2/documents/{ids}` | `clients/workspace/client.go` | Удалить документы воркспейса |
| PATCH | `internal/v2/documents/restore` | `clients/workspace/client.go` | Восстановить документы воркспейса |
### Transmittals — `TRANSMITTALS_BASE_URL`
Клиент создаётся только если переменная задана.
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `/internal/v1/transmittals/by_bundle_ids` | `pkg/transmittal/client.go` | Трансмитталы по списку bundle-id |
### Automation — `AUTOMATION_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| — | `/internal/v1/automations/{process_name}` | `pkg/automation/client.go` | Запуск/получение автоматизации по имени процесса |
### Marks (штампы/маркировки)
Режим выбирается флагом `USE_MARKS_RABBITMQ`.
| Метод | Путь / транспорт | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `/api/v1/marks/{bundle_id}` (HTTP, `MARKS_PROCESSING_URL`) | `pkg/marks/base/client.go` | Наложение штампов на бандл (HTTP-режим) |
| — | RabbitMQ (`MARKS_RABBITMQ_*`) | `pkg/marks/rpc` | Наложение штампов через очередь (RPC-режим, не HTTP) |
### BIM-API v1 — `BIM_API_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `/internal/v1/targets/{target_id}/bims-pdm` | `clients/bim-api/client.go` | Создать BIM для target (PDM) |
| POST | `/internal/v1/targets/{target_id}/bims-v2-pdm` | `clients/bim-api/client.go` | Создать BIM v2 для target (PDM) |
### BIM-API v2 (bim-core-api) — `BIM_API_V2_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `/internal/v1/projects/{project_id}/bims` | `clients/bim-api-v2/client.go` | Создать BIM для проекта |
### System log — `SYSTEM_LOG_URL`
| Метод | Путь | Клиент | Назначение |
| --- | --- | --- | --- |
| POST | `/api/v0/system_log` | `pkg/system_log/client.go` | Отправить запись в системный лог |
## Обработка ошибок
Клиенты, как правило, проверяют код ответа и оборачивают ошибку через `github.com/rotisserie/eris` (напр. «invalid response code %d expected 200»). Для случая недоступности исходного сервиса в самом API определён нестандартный статус `523` (`network/consts.go`, `StatusOriginIsUnreachable`).

View File

@ -0,0 +1,131 @@
# =============================================================================
# documentations-api / documentations-filestream — пример переменных окружения
# =============================================================================
# Все переменные читаются напрямую из окружения процесса библиотекой
# kelseyhightower/envconfig (config/config.go). Префикса и вложенности НЕТ
# имена плоские. Значения ниже — ориентир для локального запуска (аналог
# .docker/.env). Оба бинарника (api и filestreamer) используют один и тот же
# набор переменных.
# --- Адреса прослушивания ----------------------------------------------------
API_ADDRESS=0.0.0.0:6666 # порт основного API (cmd/api)
API_ADDRESS_FILE=0.0.0.0:7777 # порт файлового стримера (cmd/filestreamer)
# --- PostgreSQL --------------------------------------------------------------
POSTGRES_ADDRESS=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=documentations
POSTGRES_POOL_SIZE=10 # учитывается только в api; filestreamer жёстко использует 50
ENABLE_SSL=0 # 1 — TLS к БД, тогда нужен YC-PG-CERTIFICATE
# YC-PG-CERTIFICATE= # PEM CA-сертификат PostgreSQL (в проде — из секрета)
# --- S3 (объектное хранилище) ------------------------------------------------
ENABLE_S3=0 # в api при 0 сервис стартует без S3; в filestreamer S3 обязателен
S3_SERVICE_ACCOUNT=/etc/sarex/yc_s3_doc_account.json # путь к JSON сервис-аккаунта
# S3_SERVICE_ACCOUNT_STR= # альтернатива: сам JSON строкой
# --- Sarex backend (Django) --------------------------------------------------
DJANGO_HOST=https://stage.sarex.io
DJANGO_BASIC_AUTH= # basic-auth для системных вызовов Django
DJANGO_BASIC_AUTH_FOR_GET_USER= # отдельный basic-auth для получения пользователей
DJANGO_ORIGINATOR=docs_local
# --- Zitadel (опциональная аутентификация) -----------------------------------
USE_ZITADEL=0
ZITADEL_DOMAIN=idp.dev.stage.sarex.io
ZITADEL_ACCOUNT=/etc/sarex/zitadel/zitadel-account.json
# --- Внешние сервисы (базовые URL) -------------------------------------------
FILE_URL_EXTERNAL=https://stage-api.sarex.io/files
DOCUMENTATION_URL=http://localhost:8000/
WORKFLOW_URL=https://stage-api.sarex.io/workflows/api
WORKSPACE_URL=https://stage-api.sarex.io/worspace/api
WORKSPACE_V2_EXTERNAL_URL=https://stage.sarex.io/workspaces-v2/
WORKSPACE_BUNDLE_VERSION=v1
MARKS_PROCESSING_URL=http://marks-service.documentations:8000
BIM_API_URL=http://bim-api-service.bim-api-stage/
BIM_API_V2_URL=http://bim-core-api.platform.svc.cluster.local:8000/
BIM_API_URL_EXTERNAL=https://stage-api.sarex.io/bim
SYSTEM_LOG_URL=http://localhost:8888
FLOWS_URL=http://backend-service.proc.svc.cluster.local:8000
AUTOMATION_URL=http://automation-api-service.automation-stage.svc.cluster.local:8000
# TRANSMITTALS_BASE_URL=http://transmittal-service.documentations:8000 # опционально
# --- Публичные ссылки на документы -------------------------------------------
PUBLIC_LINK_HOST=https://document-link.stage.sarex.io
PUBLIC_KEY= # PEM RSA public key (PKIX) для проверки JWT
DOCUMENT_PUBLIC_LINK_JWT_SECRET= # секрет HMAC для JWT публичных ссылок/временных загрузок
DOCUMENT_PUBLIC_LINK_JWT_EXPIRATION_MINUTES=5
PUBLIC_LINK_FOLDER_CONNECTOR_ENABLED=1
# --- Подпись ссылок скачивания (signature-in-URL) ----------------------------
ENABLE_SIGNATURE_IN_URL=false # при true обязателен рабочий Valkey и SIGNATURE_SECRET_KEY
SIGNATURE_SECRET_KEY=
SIGNATURE_IN_URL_EXPIRATION_SECONDS=600
ENABLE_AUTH_JWT_IN_URL=true
# --- Valkey / Redis (кэши, кэш подписей) -------------------------------------
VALKEY_ADDR=localhost:6380 # host:port; пустая строка полностью отключает клиент
VALKEY_LOGIN=
VALKEY_PASSWORD=
VALKEY_HOST=
VALKEY_DB=0
VALKEY_CACHE_TTL=1h
VALKEY_SSL=false
VALKEY_SSL_CA_CERTS=
# --- Файловый стример / кэш --------------------------------------------------
READ_WRITE_TIMEOUT_FILE_STREAM=6h
USE_CACHE_IN_FILE_STREAMER=true
CACHE_DEFAULT_EXPIRATION=30s
CACHE_CLEANUP_INTERVAL=35s
# --- BIM / обработка ---------------------------------------------------------
USE_BIMV1_FOR_BIMV2=1
USE_BIM_INSERTER=1
USE_LEGACY_BIM_FLOW=0
LAST_MASTER_BIM=1541
LAST_SLAVE_1_BIM=5000
LAST_SLAVE_2_BIM=15000
CONVERT_DWG_TO_GEOJSON=true
CONVERT_DXF_TO_GEOJSON=true
IS_CONVERTED_PDF_UPLOADING_TO_S3=true
DELETE_S3D_AFTER_MESHOPT=false
WORKFLOW_IMAGES_VERSION=develop
WORKFLOWS_IMAGES_VERSION=develop
CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q
WORKFLOWS_CONFIG_FILEPATH=.example.tasks_execution_config.json
# --- Штампы/маркировки: HTTP или RabbitMQ ------------------------------------
USE_MARKS_RABBITMQ=0 # 0 — ходить в MARKS_PROCESSING_URL по HTTP; 1 — через RabbitMQ
MARKS_RABBITMQ_HOST=
MARKS_RABBITMQ_PORT=
MARKS_RABBITMQ_USER=
MARKS_RABBITMQ_PASSWORD=
MARKS_RABBITMQ_API=
# --- Rate limit для /documents/metadata --------------------------------------
METADATA_RATE_LIMIT_ENABLED=true
METADATA_RATE_LIMIT_MAX_REQUESTS=10
# --- Почта (флаги; сама отправка идёт через внешние сервисы) ------------------
ENABLE_MAILGUN=true
ENABLE_SMTP=false
# --- Наблюдаемость -----------------------------------------------------------
ENVIRONMENT=stage
SENTRY_DSN=
SENTRY_DEBUG=0
NAMESPACE=local
ENABLE_SQL_QUERY=1 # логировать SQL-запросы (для локальной отладки)
TRACER_USE=false
TRACER_HOST=localhost:4317
TRACER_USE_INSECURE=true
TRACER_LOGGER_NAME=tracer_logger
SERVICE_NAME=documentations-api
SERVICE_NAME_FILESTREAM=filestream-api
# --- Только для интеграционных тестов (кодом приложения не читается) ----------
# KEY_JWT=example_jwt

View File

@ -0,0 +1,785 @@
openapi: 3.0.3
info:
title: Documentations API
version: "1.0"
description: |
REST-API сервиса документаций (`documentation-api`, бинарник `cmd/api`, образ
`documentations`, deployment `documentations-api` в неймспейсе `documentations`).
Спецификация реконструирована из исходного кода маршрутов
(`cmd/api/routes_api.go`, `cmd/api/routes_internal.go`) и middleware
(`cmd/api/bootstrap.go`, `pkg/midleware/*`). В репозитории **нет сгенерированного
swagger/openapi**, поэтому схемы тел запросов/ответов приведены обобщённо
(в коде они не описаны декларативно). Пути, методы и параметры пути —
достоверные, из роутера `gorilla/mux`.
## Базовые пути
- Публичный API: `/api/v1` (описан ниже).
- Внутренний API: `/internal/v1` (сервис-к-сервису, облегчённая авторизация;
здесь не детализируется — см. `cmd/api/routes_internal.go`).
- Потоковая отдача/приём файлов вынесены в **отдельный сервис `filestream`**
(`cmd/filestreamer`, образ `documentations-api-files`): `/api/v1/bundles/...`,
`/api/v1/documents/...`, `/api/v1/pages/...`, `/api/v1/documents/folders`,
`/api/v1/bundles_mrpas/...`, `/api/v1/public_link_mrpas/...`.
- Health-check: `GET /ping` (предоставляется каркасом роутера `rest`).
- Профилирование: `GET /debug/pprof/...` (net/http/pprof).
## Аутентификация
Основной способ — JWT в заголовке `Authorization: Bearer <token>`, проверяемый по
RSA-публичному ключу (`PUBLIC_KEY`, PEM/PKIX). Из claims извлекаются `company_ids`
и `service_accounts`. Опционально включается проверка через Zitadel (`USE_ZITADEL`),
а также разбор заголовка `Identity`. Часть ручек (пути `/public/...`,
`/public_link_mrpas/...` и запросы с `download_type=temporary`) авторизуются по
HMAC-JWT публичных ссылок (`DOCUMENT_PUBLIC_LINK_JWT_SECRET`). В сервисе filestream
ссылки скачивания дополнительно подписываются (`signature` + `expires_at` в query,
секрет `SIGNATURE_SECRET_KEY`).
## Формат ошибок
Ответы оборачиваются middleware `rest.JSONResponse`; ошибки возвращаются в JSON.
Нестандартный код `523` (`StatusOriginIsUnreachable`, `network/consts.go`)
используется, когда исходный сервис недоступен.
## Пагинация
Единого декларативного механизма пагинации в роутере нет; списки, где она нужна,
принимают параметры фильтрации в теле POST-запроса (напр. `/documents/metadata`,
`/documents/batch`). Эндпоинт `/documents/metadata` дополнительно ограничивается
rate-limit (`METADATA_RATE_LIMIT_*`).
servers:
- url: https://api.sarex.io/documentations/api/v1
description: production
- url: https://api.preprod.sarex.io/documentations/api/v1
description: preprod
- url: https://stage-api.sarex.io/documentations/api/v1
description: stage
security:
- bearerAuth: []
tags:
- name: disks
- name: documents
- name: bundles
- name: uploads
- name: permissions
- name: workspaces
- name: dashboards
- name: workflows
- name: pages
- name: marks
- name: public-links
- name: related-documents
- name: changelogs
- name: favorite-documents
- name: name-templates
- name: misc
paths:
/conversion:
post:
tags: [misc]
summary: Запустить конвертацию документа
responses:
"200": { $ref: "#/components/responses/Ok" }
/disks:
get:
tags: [disks]
summary: Список дисков (доступных пользователю)
responses:
"200": { $ref: "#/components/responses/Ok" }
post:
tags: [disks]
summary: Создать диск (требуются права администратора)
responses:
"200": { $ref: "#/components/responses/Ok" }
"403": { $ref: "#/components/responses/Forbidden" }
/disks/{disk_id}:
delete:
tags: [disks]
summary: Удалить диск (требуются права администратора)
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
"403": { $ref: "#/components/responses/Forbidden" }
/disks/{disk_id}/documents:
get:
tags: [disks, documents]
summary: Документы диска
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
post:
tags: [disks, documents]
summary: Документы диска по списку id
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/disks/{disk_id}/projects:
get:
tags: [disks]
summary: Проекты диска
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/disks/{disk_id}/service_accounts:
get:
tags: [disks, permissions]
summary: Сервисные аккаунты диска
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/disks/{disk_id}/size_migration:
get:
tags: [misc]
summary: Миграция размеров (служебное)
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/disks/{disk_id}/delete_documents_from_ws_migration:
get:
tags: [misc]
summary: Удаление документов при миграции воркспейса (служебное)
parameters: [{ $ref: "#/components/parameters/DiskId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents:
post:
tags: [documents]
summary: Создать документ/папку
responses:
"200": { $ref: "#/components/responses/Ok" }
delete:
tags: [documents]
summary: Массовое удаление документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/create_report:
post:
tags: [documents]
summary: Сформировать отчёт по метаданным документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/metadata:
post:
tags: [documents]
summary: Список метаданных документов (rate-limited)
responses:
"200": { $ref: "#/components/responses/Ok" }
"429": { $ref: "#/components/responses/TooManyRequests" }
/documents/batch:
post:
tags: [documents]
summary: Пакетное получение документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/flows:
post:
tags: [documents]
summary: Документы в трансмиттале/ревью
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/public_link:
post:
tags: [public-links]
summary: Создать публичную ссылку на документ
responses:
"200": { $ref: "#/components/responses/Ok" }
/public/documents/public_link/{id}:
get:
tags: [public-links]
summary: Прочитать публичную ссылку (публичный доступ по HMAC-JWT)
security: []
parameters: [{ $ref: "#/components/parameters/StrId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/public_link/{id}:
patch:
tags: [public-links]
summary: Обновить публичную ссылку
parameters: [{ $ref: "#/components/parameters/StrId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
delete:
tags: [public-links]
summary: Удалить публичную ссылку
parameters: [{ $ref: "#/components/parameters/StrId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/update-path:
patch:
tags: [documents]
summary: Сменить родителя документа
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/update-path:
patch:
tags: [documents]
summary: Массовая смена родителя документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/super_create:
post:
tags: [documents]
summary: Создание документа суперпользователем (требуются права администратора)
responses:
"200": { $ref: "#/components/responses/Ok" }
"403": { $ref: "#/components/responses/Forbidden" }
/documents/types:
get:
tags: [documents]
summary: Справочник типов документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/get_folders_download_url:
get:
tags: [documents]
summary: Ссылка на скачивание папок (подписанная)
responses:
"200": { $ref: "#/components/responses/Ok" }
/download_url/documents:
get:
tags: [documents]
summary: Ссылка на скачивание документа
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/name_template:
get:
tags: [documents, name-templates]
summary: Шаблон имени документа
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}:
get:
tags: [documents]
summary: Документ по id
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [documents]
summary: Переименовать/изменить документ (числовой id)
parameters: [{ $ref: "#/components/parameters/DocumentIdNum" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
delete:
tags: [documents]
summary: Удалить документ (числовой id)
parameters: [{ $ref: "#/components/parameters/DocumentIdNum" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/bundles:
get:
tags: [documents, bundles]
summary: Бандлы документа
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/add_bundle:
post:
tags: [documents, bundles]
summary: Привязать бандл к документу
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/move_bundles:
patch:
tags: [documents, bundles]
summary: Переместить бандлы
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/ancestors:
get:
tags: [documents]
summary: Предки документа
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/filetypes_by_extension:
post:
tags: [documents]
summary: Определить тип файла по расширению
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/copy:
post:
tags: [documents]
summary: Копировать документы (долгая операция, таймаут 120 мин)
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/copy_structure:
post:
tags: [documents]
summary: Копировать структуру папок
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/bin:
delete:
tags: [documents]
summary: Окончательно удалить документы из корзины
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/bin/restore:
patch:
tags: [documents]
summary: Восстановить документы из корзины
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_ids}/company:
get:
tags: [documents]
summary: Компания документов
parameters:
- name: document_ids
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/permissions:
get:
tags: [permissions]
summary: Права доступа документа
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
post:
tags: [permissions]
summary: Выдать права на документ
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/documents/{document_id}/download:
get:
tags: [documents]
summary: Скачать документ (отдаётся сервисом filestream)
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/permissions:
get:
tags: [permissions]
summary: Справочник прав доступа
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles:
post:
tags: [bundles]
summary: Создать бандл
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}:
get:
tags: [bundles]
summary: Бандл по id
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
patch:
tags: [bundles]
summary: Изменить бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
delete:
tags: [bundles]
summary: Удалить бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/download:
get:
tags: [bundles]
summary: Скачать бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/{bundle_key}/download:
get:
tags: [bundles]
summary: Скачать файл бандла по ключу
parameters:
- { $ref: "#/components/parameters/BundleId" }
- { $ref: "#/components/parameters/BundleKey" }
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/{bundle_key}/upload_single:
post:
tags: [bundles, uploads]
summary: Загрузить файл целиком (single upload)
parameters:
- { $ref: "#/components/parameters/BundleId" }
- { $ref: "#/components/parameters/BundleKey" }
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/{bundle_key}/upload_multipart:
post:
tags: [bundles, uploads]
summary: Начать multipart-загрузку файла бандла
parameters:
- { $ref: "#/components/parameters/BundleId" }
- { $ref: "#/components/parameters/BundleKey" }
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/upload_finish:
post:
tags: [bundles, uploads]
summary: Завершить загрузку бандла
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/marks:
put:
tags: [marks]
summary: Добавить штампы/QR/подписи в бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/sign:
post:
tags: [bundles, marks]
summary: Подписать бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/restart:
post:
tags: [bundles, workflows]
summary: Перезапустить workflow бандла
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/cancel_qr:
patch:
tags: [marks]
summary: Отменить QR-код
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/comment:
patch:
tags: [bundles]
summary: Обновить комментарий бандла
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/presigned_url:
get:
tags: [bundles]
summary: Presigned URL бандла
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/mrpas:
get:
tags: [bundles]
summary: MRPA бандла
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundles/{bundle_id}/copy:
post:
tags: [bundles]
summary: Копировать бандл
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/bundle/version:
get:
tags: [bundles]
summary: Версии бандла
responses:
"200": { $ref: "#/components/responses/Ok" }
/uploads/multipart/{upload_id}/complete:
post:
tags: [uploads]
summary: Завершить multipart-загрузку
parameters: [{ $ref: "#/components/parameters/UploadId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/uploads/multipart/{upload_id}/abort:
post:
tags: [uploads]
summary: Прервать multipart-загрузку
parameters: [{ $ref: "#/components/parameters/UploadId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/uploads/multipart/{upload_id}/{part_num}:
post:
tags: [uploads]
summary: Загрузить часть (part) файла
parameters:
- { $ref: "#/components/parameters/UploadId" }
- name: part_num
in: path
required: true
schema: { type: integer }
responses:
"200": { $ref: "#/components/responses/Ok" }
/workspaces:
post:
tags: [workspaces]
summary: Создать воркспейс
responses:
"200": { $ref: "#/components/responses/Ok" }
/workspaces/{ws_id}:
get:
tags: [workspaces]
summary: Документ воркспейса
parameters:
- name: ws_id
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/dashboards:
post:
tags: [dashboards]
summary: Создать дашборд
responses:
"200": { $ref: "#/components/responses/Ok" }
/dashboards/{db_id}:
get:
tags: [dashboards]
summary: Документ дашборда
parameters:
- name: db_id
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/workflows/{workflow_id}:
get:
tags: [workflows]
summary: Workflow по id
parameters:
- name: workflow_id
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/pages:
post:
tags: [pages]
summary: Создать страницу
responses:
"200": { $ref: "#/components/responses/Ok" }
/pages/{data_source}/{page_key}/download:
get:
tags: [pages]
summary: Скачать страницу
parameters:
- name: data_source
in: path
required: true
schema: { type: string }
- name: page_key
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/public/qr/{public_uuid}/document_info:
get:
tags: [marks]
summary: Публичная информация о документе по QR
security: []
parameters:
- name: public_uuid
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200": { $ref: "#/components/responses/Ok" }
/related_documents:
post:
tags: [related-documents]
summary: Создать связь документов
responses:
"200": { $ref: "#/components/responses/Ok" }
get:
tags: [related-documents]
summary: Получить связанные документы
responses:
"200": { $ref: "#/components/responses/Ok" }
/related_documents/bulk_delete:
post:
tags: [related-documents]
summary: Массово удалить связи документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/templates/{bundle_id}:
get:
tags: [misc]
summary: Шаблон по бандлу
parameters: [{ $ref: "#/components/parameters/BundleId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/changelogs/create:
post:
tags: [changelogs]
summary: Создать changelog
responses:
"200": { $ref: "#/components/responses/Ok" }
/changelogs/{changelog_id}:
patch:
tags: [changelogs]
summary: Обновить changelog
parameters:
- name: changelog_id
in: path
required: true
schema: { type: string }
responses:
"200": { $ref: "#/components/responses/Ok" }
/links:
post:
tags: [misc]
summary: Создать ссылку
responses:
"200": { $ref: "#/components/responses/Ok" }
/favorite_documents:
post:
tags: [favorite-documents]
summary: Добавить документ в избранное
responses:
"200": { $ref: "#/components/responses/Ok" }
get:
tags: [favorite-documents]
summary: Список избранных документов
responses:
"200": { $ref: "#/components/responses/Ok" }
/favorite_documents/{document_id}:
delete:
tags: [favorite-documents]
summary: Убрать документ из избранного
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
/name_templates/create:
post:
tags: [name-templates]
summary: Создать шаблон имени
responses:
"200": { $ref: "#/components/responses/Ok" }
/name_templates/{document_id}:
get:
tags: [name-templates]
summary: Шаблон имени по документу
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
patch:
tags: [name-templates]
summary: Обновить шаблон имени
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
delete:
tags: [name-templates]
summary: Удалить шаблон имени
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
responses:
"200": { $ref: "#/components/responses/Ok" }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
JWT, подписанный ключом, соответствующим `PUBLIC_KEY` (RSA/PKIX).
Для публичных ссылок используется HMAC-JWT (`DOCUMENT_PUBLIC_LINK_JWT_SECRET`).
parameters:
DiskId:
name: disk_id
in: path
required: true
schema: { type: string }
DocumentId:
name: document_id
in: path
required: true
schema: { type: string }
DocumentIdNum:
name: document_id
in: path
required: true
description: Числовой идентификатор документа (маршрут ограничен regex `[0-9]+`)
schema: { type: integer }
BundleId:
name: bundle_id
in: path
required: true
schema: { type: string, format: uuid }
BundleKey:
name: bundle_key
in: path
required: true
schema: { type: string }
UploadId:
name: upload_id
in: path
required: true
schema: { type: string }
StrId:
name: id
in: path
required: true
schema: { type: string }
responses:
Ok:
description: Успешный ответ (тело зависит от ручки; в JSON)
content:
application/json:
schema: { type: object, additionalProperties: true }
Forbidden:
description: Недостаточно прав
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
NotFound:
description: Ресурс не найден
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
TooManyRequests:
description: Превышен лимит запросов (rate limit)
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
schemas:
Error:
type: object
properties:
error:
type: string
message:
type: string
additionalProperties: true

View File

@ -0,0 +1,156 @@
# Конфигурация проекта dps-message-hub
# Версия: 0.1.0
Документ описывает все переменные окружения и способы конфигурирования сервиса.
`dps-message-hub` (`dps_message_hub`) — это Kafka-воркер на базе [FastStream](https://faststream.airt.ai/), потребляющий сообщения об изменении ассетов и обновляющий данные разметки (`markup_event`) в PostgreSQL домена «documentations». HTTP API у сервиса нет.
> Это отдельный сервис, не путать с приложением `message-hub` (домен `planning`): у них разные префиксы переменных, набор интеграций и назначение.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/dps_message_hub/infra/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `AppSettings(BaseSettings)`).
Особенности разбора (`SettingsConfigDict`):
- `env_prefix="DPS_MESSAGE_HUB_"` — все переменные приложения начинаются с этого префикса;
- `env_nested_delimiter="__"` — вложенные секции задаются двойным подчёркиванием, напр. `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST``documentation_db.host`;
- `env_ignore_empty=False` — пустая строка считается заданным значением (не игнорируется);
- `frozen=True` — объект настроек неизменяем после инициализации.
Настройки разбиты на три вложенные секции (модели `pydantic.BaseModel`), читаемые одним классом `AppSettings`:
- `app` (`App`) — префикс `DPS_MESSAGE_HUB_APP__`;
- `documentation_db` (`Database`) — префикс `DPS_MESSAGE_HUB_DOCUMENTATION_DB__`;
- `kafka` (`Kafka`) — префикс `DPS_MESSAGE_HUB_KAFKA__`.
Класс `Settings` (`src/dps_message_hub/infra/config.py`) — синглтон (`wiring.SingletonMeta`) поверх `AppSettings`, отдаёт настройки через свойство `.settings`.
Отдельного конфиг-файла (yaml/toml) у приложения нет. У классов настроек **не задан** `env_file`, поэтому файл `.env` автоматически не загружается — переменные нужно экспортировать в окружение процесса самостоятельно (`make config` лишь копирует `.example.env``.env` как шаблон), либо пробрасывать их в контейнер через `--env-file` (см. `Makefile`, цель `container-run`).
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. `make config` копирует `.example.env``.env`, но приложение **не загружает `.env` автоматически** — экспортируйте сами, напр. `set -a && . ./.env && set +a` |
| Локально (контейнер) | `Makefile`: цель `container-run` пробрасывает переменные через `--env-file .env` |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `universal-chart.services.api.envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов); базовый чарт — `universal-chart` (`oci://.../charts`, версия `0.1.7`) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и `HELM_SET_ARGS` |
Запуск процесса (`scripts/entrypoint.sh`): единый FastStream-процесс с брокером Kafka:
```
faststream run \
--factory \
--workers ${DPS_MESSAGE_HUB_NUM_WORKERS} \
'dps_message_hub.infra.app:get_app'
```
Фабрика `dps_message_hub.infra.app:get_app` (`src/dps_message_hub/infra/app.py`) собирает `FastStream`-приложение: создаёт `KafkaBroker`, подключает роутер-потребитель топика `assets` и открывает пул соединений PostgreSQL в lifespan. Отдельных точек входа для воркеров/крон-задач нет — `pyproject.toml` не содержит `[project.scripts]`.
## Переменные приложения
Дефолт `—` означает, что значение обязательно (иначе ошибка старта настроек).
### Приложение (`DPS_MESSAGE_HUB_APP__*`) — класс `App`
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DPS_MESSAGE_HUB_APP__LOG_LEVEL` | enum (`LogLevel`) | `INFO` | Уровень логирования. Допустимо: `CRITICAL`/`FATAL`/`ERROR`/`WARNING`/`WARN`/`INFO`/`DEBUG`/`NOTSET` |
| `DPS_MESSAGE_HUB_APP__IS_DEV` | bool | `False` | Признак dev-режима. Поле объявлено в настройках, но в текущем коде не используется |
| `DPS_MESSAGE_HUB_APP__BROKER_TYPE` | enum (`BrokerType`) | `—` (обязательно) | Тип брокера сообщений. Поддерживается только значение `kafka` |
### База данных PostgreSQL (`DPS_MESSAGE_HUB_DOCUMENTATION_DB__*`) — класс `Database`
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST` | string | `—` | Хост PostgreSQL |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PORT` | int | `—` | Порт PostgreSQL |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER` | string | `—` | Пользователь БД |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD` | string | `—` | Пароль пользователя БД |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__NAME` | string | `—` | Имя базы данных |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL` | bool | `—` | Включить TLS-подключение к БД |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_MODE` | enum (`verify-full`/`verify-ca`/`""`) | `—` | Режим проверки TLS (параметр `sslmode` DSN). При `ENABLE_SSL=true` не должно быть пустым |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH` | string | `—` | Путь к CA-сертификату (параметр `sslrootcert` DSN). При `ENABLE_SSL=true` не должно быть пустым |
Валидатор `check_ssl_options_configured_when_ssl_enabled`: если `ENABLE_SSL=true`, то `SSL_MODE` и `SSL_ROOT_CERT_PATH` обязаны быть непустыми, иначе ошибка старта. Итоговый DSN собирается в вычисляемом поле `documentation_db.uri` (`postgresql://user:password@host:port/name`, при SSL добавляются `?sslmode=...&sslrootcert=...`).
### Kafka (`DPS_MESSAGE_HUB_KAFKA__*`) — класс `Kafka`
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DPS_MESSAGE_HUB_KAFKA__HOST` | string | `—` | Хост брокера Kafka |
| `DPS_MESSAGE_HUB_KAFKA__PORT` | int | `—` | Порт брокера Kafka |
| `DPS_MESSAGE_HUB_KAFKA__USERNAME` | string | `—` | Логин SASL (`SCRAM-SHA-512`) |
| `DPS_MESSAGE_HUB_KAFKA__PASSWORD` | string | `—` | Пароль SASL (`SCRAM-SHA-512`) |
| `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` | string \| null | `None` | Путь к CA-сертификату для SSL-контекста. Если задан — используется `SASL_SSL`, иначе SASL без TLS |
| `DPS_MESSAGE_HUB_KAFKA__TOPICS` | dict (JSON) → `KafkaTopics` | `—` | Соответствие логического топика реальному имени в Kafka. Обязателен ключ `assets`, напр. `{"assets": "assets_broadcast"}` |
Формирование параметров подключения (`src/dps_message_hub/infra/app.py`): адрес брокера — вычисляемое поле `kafka.uri` (`host:port`). Безопасность через `faststream.security.SASLScram512`:
- если заданы `USERNAME` и `PASSWORD` и `SSL_CAFILE` пуст — `SASLScram512(..., use_ssl=False)`;
- если задан `SSL_CAFILE``SASLScram512(..., ssl_context=create_ssl_context(cafile=...))`.
## Переменные инфраструктуры, сборки и запуска
Не читаются классами настроек приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `DPS_MESSAGE_HUB_NUM_WORKERS` | `scripts/entrypoint.sh`, Helm `envs` | Число воркеров FastStream (`faststream run --workers`). В Helm `_default: '2'` |
| `DPS_MESSAGE_HUB_PYTHON_IMAGE_NAME` | `Dockerfile` (ARG) | Базовый образ Python (по умолчанию `python`) |
| `DPS_MESSAGE_HUB_PYTHON_IMAGE_TAG` | `Dockerfile` (ARG) | Тег базового образа (по умолчанию `3.13-slim`) |
| `PIP_INDEX_URL`, `PIP_TRUSTED_HOST` | `Dockerfile` (ARG) | Индекс/доверенный хост pip при сборке |
Сборка (`Dockerfile`): многостадийная — стадия `builder` собирает wheel'ы из `requirements/requirements.txt`, стадия `runner` ставит их, копирует `src/` и устанавливает пакет (`pip install . --no-deps`); процесс запускается непривилегированным пользователем `dps_message_hub`. Целевая версия Python — `3.13` (`.python-version`, `requires-python >=3.13`).
## Переменные из Helm-чарта (`.helm/values.yaml`)
Сервис деплоится через зависимость `universal-chart` (`.helm/Chart.yaml`, версия `0.1.7`). Настраивается один сервис — `services.api` (тип нагрузки — deployment, `replicaCount._default: 1`). HTTP-`service` и `ingress` отключены, health-пробы (`liveness`/`readiness`, путь `/ping`) — `enabled: false` (у сервиса нет HTTP-эндпоинтов).
Обычные значения (блок `services.api.envs`) различаются по окружениям (`_default`/`stage`/`preprod`/`production`) адресами БД и Kafka, именем БД и именами топиков. В Helm дополнительно заданы (отсутствуют в `.example.env`): `DPS_MESSAGE_HUB_KAFKA__HOST`, `DPS_MESSAGE_HUB_KAFKA__PORT` (`9091`), `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` (`/opt/config/ca.crt`), `DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL=true`, `SSL_MODE=verify-full`, `SSL_ROOT_CERT_PATH=/opt/config/ca.crt`.
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`_default` / `stage`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER` | `ya-pg-secret` / `documentations-postgresql-secret` | `username` |
| `DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD` | `ya-pg-secret` / `documentations-postgresql-secret` | `password` |
| `DPS_MESSAGE_HUB_KAFKA__USERNAME` | `kafka-secret` | `username` |
| `DPS_MESSAGE_HUB_KAFKA__PASSWORD` | `kafka-secret` | `password` |
Помимо env, чарт монтирует CA-сертификат Яндекса из `ConfigMap` `ya-ca-cert` (ключ `ca.crt`) как файл `/opt/config/ca.crt` (том `cm-ya-ca-cert`, `readOnly`) — на него указывают `DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE` и `DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH |
| --- | --- | --- | --- | --- |
| MR (`merge_request_event`) | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | `stage` |
| ветка `master` | `preprod` | `documentations-preprod` | `0.0.1-preprod` | `preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `0.0.1-prod` | `master` |
Ключевые переменные пайплайна: `SERVICE_NAME=dps-message-hub`, `DOCKERFILE_PATH=Dockerfile`, `CI_TRIGGER_SOURCE=app`, `RELEASE_NAME=dps-message-hub`, `CHART_NAME=dps-message-hub`, флаги `ENABLE_BUILD_CHART`/`ENABLE_BUILD_IMAGE`/`ENABLE_DEPLOY`, `HELM_SET_ARGS` (`--set universal-chart...`). Отдельные job'ы `format` (`ruff format --diff`) и `lint` (`ruff check`) на образе `python:3.13-slim`.
## Замечания и потенциальные проблемы
- Приложение **не читает `.env`** автоматически (в `SettingsConfigDict` нет `env_file`). `make config` только создаёт `.env` из шаблона — переменные нужно экспортировать самому либо передавать контейнеру через `--env-file`.
- Большинство полей БД и Kafka **обязательны** (без дефолтов): при пустом окружении настройки не пройдут валидацию и сервис не стартует. Единственные необязательные — `APP__LOG_LEVEL`, `APP__IS_DEV`, `KAFKA__SSL_CAFILE`.
- `DPS_MESSAGE_HUB_APP__BROKER_TYPE` обязателен; поддерживается только `kafka` (иных веток в `match` нет). При другом значении брокер не будет создан.
- При `ENABLE_SSL=true` обязательно задавать `SSL_MODE` и `SSL_ROOT_CERT_PATH`, иначе валидатор настроек прервёт старт.
- `DPS_MESSAGE_HUB_KAFKA__TOPICS` обязан содержать ключ `assets` (модель `KafkaTopics`); прочие ключи игнорируются, отсутствие `assets` — ошибка старта.
- Поле `APP__IS_DEV` присутствует в настройках, но в текущем коде нигде не задействовано.
- Обработка Kafka-сообщений идёт с `auto_commit=False` и middleware `Retry` (`src/dps_message_hub/interface/middleware.py`), которая повторяет обработку **бесконечно** с экспоненциальной задержкой (до `1<<10 = 1024` сек) — «отравленное» сообщение может заблокировать партицию.
## Минимальный набор для локального запуска
Kafka и Zookeeper поднимаются через `Makefile` (цели `run-deps`/`run-zookeeper`/`run-kafka`); PostgreSQL — внешний. Минимально нужно задать:
- `DPS_MESSAGE_HUB_APP__BROKER_TYPE=kafka`;
- `DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST`, `__PORT`, `__USER`, `__PASSWORD`, `__NAME`, `__ENABLE_SSL` (для локали `false`), `__SSL_MODE`, `__SSL_ROOT_CERT_PATH` (при `ENABLE_SSL=false` можно пустыми);
- `DPS_MESSAGE_HUB_KAFKA__HOST`, `__PORT`, `__USERNAME`, `__PASSWORD`, `__TOPICS` (JSON с ключом `assets`);
- `DPS_MESSAGE_HUB_NUM_WORKERS` (для `entrypoint.sh`).
Локальный запуск: `make run` (через `entrypoint.sh`) либо `make run-dev` (`faststream run --factory --reload dps_message_hub.infra.app:get_app`). Готовые значения-примеры приведены в `dps-message-hub.env.example` рядом с этим документом.

View File

@ -0,0 +1,45 @@
# Интерфейсы сервиса dps-message-hub
# Версия: 0.1.0
Документ описывает интерфейсную поверхность сервиса: потребляемые топики Kafka и обращения к внешним зависимостям (PostgreSQL).
> `dps-message-hub` — это чистый Kafka-воркер на [FastStream](https://faststream.airt.ai/). У него **нет HTTP/REST API** (в коде нет FastAPI/Flask/aiohttp, health-роутов и т.п.), поэтому файла `openapi.yaml` для сервиса нет. Исходящих HTTP-запросов к другим сервисам он тоже не выполняет — единственный получатель данных — база PostgreSQL.
## Как устроено взаимодействие
Единое FastStream-приложение (`dps_message_hub.infra.app:get_app`, `src/dps_message_hub/infra/app.py`) объединяет:
- **Kafka** — потребитель сообщений (`KafkaBroker` + `KafkaRouter`), топик `assets` (`src/dps_message_hub/features/assets/interface/kafka.py`);
- **PostgreSQL** — пул соединений `psycopg` (`AsyncConnectionPool`), открывается/закрывается в lifespan (`src/dps_message_hub/infra/lifespan.py`, `infra/database.py`).
Каждое сообщение проходит через middleware `Retry` (`src/dps_message_hub/interface/middleware.py`): при исключении обработка повторяется бесконечно с экспоненциальной задержкой (`1 << min(10, retry_count)` секунд). Коммит оффсета ручной у потребителя `auto_commit=False`.
## Kafka-потребители (входящие сообщения)
Реальное имя топика задаётся переменной `DPS_MESSAGE_HUB_KAFKA__TOPICS` (маппинг логического имени `assets` в имя топика Kafka). Формат сообщения — `BrokerMessageDto` (`src/dps_message_hub/infra/dto.py`): поля `schema_version`, `model`, `sender`, `type`, `body`, `diff`, `timestamp`, `trace_id` (alias `xtraceId`), `user_id`, `tenants`, `tags`.
| Топик (логич.) | `group_id` | Offset reset | `auto_commit` | Обработчик | Назначение |
| --- | --- | --- | --- | --- | --- |
| `assets` | `dps_assets_consumer` | earliest | `false` | `assets` | Обновление событий разметки (`markup_event`) по изменению ассета |
Диспетчеризация внутри обработчика `assets` по полю `type` (`src/dps_message_hub/features/assets/interface/kafka.py`); `body` разбирается в модель `AssetUpdate` (поля `id`, `attributes[]`), `diff` — произвольный `dict`:
| `type` сообщения | Условие (`diff`) | Действие | Назначение |
| --- | --- | --- | --- |
| `model_updated` | `diff is None` | — | Пропуск (нет изменений) |
| `model_updated` | в `diff` есть ключ `resource_id` | `unlink_asset(asset_id)` | Отвязка ассета: деактивация его событий разметки |
| `model_updated` | в `diff` есть ключ `attributes` | `update_markup_events(asset_id, attributes)` | Пересоздание событий разметки по обновлённым атрибутам |
| `model_deleted` | — | `unlink_asset(asset_id)` | Отвязка ассета при удалении модели |
Бизнес-логика (`src/dps_message_hub/features/assets/application/asset.py`): для обновляемых атрибутов существующие активные `markup_event` деактивируются (`inactivation_time`), затем в транзакции вставляются новые записи. Тип атрибута (поле `type`) определяет целевую колонку значения (`value_int`/`value_float`/`value_string`/`value_option_id`/`unit_option_id`/`value_dt`/`value_date`) — маппинг в `features/assets/domain/entities.py`.
## Внешние зависимости (инфраструктура)
| Зависимость | Назначение |
| --- | --- |
| Kafka | Источник сообщений (топик `assets`). Подключение `SASLScram512`, при заданном `KAFKA__SSL_CAFILE` — по `SASL_SSL` |
| PostgreSQL | Хранилище данных (`psycopg` async, таблица `markup_event`). Операции: `SELECT`, `UPDATE`, массовая вставка `COPY ... FROM STDIN` (`features/assets/infra/database.py`) |
## Исходящие HTTP-запросы к внешним сервисам
Отсутствуют. Сервис не содержит HTTP-клиентов (`httpx`/`aiohttp`/`requests`) и не обращается к другим сервисам по HTTP; все побочные эффекты — запись в PostgreSQL.

View File

@ -0,0 +1,42 @@
# =============================================================================
# dps-message-hub — пример конфигурации (.env)
# Версия: 0.1.0
# Скопируйте в .env (`make config`) и заполните значения.
# ВНИМАНИЕ: приложение НЕ загружает .env автоматически (в классах настроек нет
# env_file/dotenv). Экспортируйте переменные сами, напр.:
# set -a && . ./.env && set +a
# либо запускайте контейнер через `--env-file .env` (см. Makefile).
# =============================================================================
# --- Инфраструктура запуска (не читается классом настроек) ---
# Число воркеров faststream (scripts/entrypoint.sh: faststream run --workers)
DPS_MESSAGE_HUB_NUM_WORKERS=1
# --- Приложение (префикс DPS_MESSAGE_HUB_APP__) ---
# CRITICAL | FATAL | ERROR | WARNING | WARN | INFO | DEBUG | NOTSET
DPS_MESSAGE_HUB_APP__LOG_LEVEL=INFO
DPS_MESSAGE_HUB_APP__IS_DEV=true
# Тип брокера сообщений (поддерживается только kafka)
DPS_MESSAGE_HUB_APP__BROKER_TYPE=kafka
# --- База данных PostgreSQL (префикс DPS_MESSAGE_HUB_DOCUMENTATION_DB__) ---
DPS_MESSAGE_HUB_DOCUMENTATION_DB__HOST=
DPS_MESSAGE_HUB_DOCUMENTATION_DB__PORT=6432
DPS_MESSAGE_HUB_DOCUMENTATION_DB__USER=
DPS_MESSAGE_HUB_DOCUMENTATION_DB__PASSWORD=
DPS_MESSAGE_HUB_DOCUMENTATION_DB__NAME=
DPS_MESSAGE_HUB_DOCUMENTATION_DB__ENABLE_SSL=false
# verify-full | verify-ca | "" (обязателен при ENABLE_SSL=true)
DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_MODE=
# Путь к CA-сертификату (обязателен при ENABLE_SSL=true)
DPS_MESSAGE_HUB_DOCUMENTATION_DB__SSL_ROOT_CERT_PATH=
# --- Kafka (префикс DPS_MESSAGE_HUB_KAFKA__) ---
DPS_MESSAGE_HUB_KAFKA__HOST=localhost
DPS_MESSAGE_HUB_KAFKA__PORT=9092
DPS_MESSAGE_HUB_KAFKA__USERNAME=
DPS_MESSAGE_HUB_KAFKA__PASSWORD=
# Путь к CA-сертификату для SSL-контекста (SASL_SSL). Если пусто — SASL без SSL
# DPS_MESSAGE_HUB_KAFKA__SSL_CAFILE=/opt/config/ca.crt
# Соответствие логического топика `assets` реальному имени топика Kafka (JSON)
DPS_MESSAGE_HUB_KAFKA__TOPICS='{"assets": "assets_broadcast_test"}'

View File

@ -0,0 +1,66 @@
# Конфигурация проекта documentation-frontend
Документ описывает сборку, рантайм-конфигурацию и деплой микрофронтенда `documentation-frontend` (образ `documentation-frontend-app`, деплой `frontend` в домене `documentations`).
## Способ конфигурирования
Это фронтенд-модуль (Module Federation remote), поэтому в отличие от backend-сервисов он **не** читает переменные окружения в рантайме. Единственный параметр конфигурации, влияющий на поведение, — окружение сборки `BUILD_ENV`, которое webpack «зашивает» в бандл на этапе сборки.
- Значение берётся из `process.env.BUILD_ENV` при запуске webpack.
- Webpack подставляет его как глобальную константу `BUILD_ENV` через `DefinePlugin` (`webpack.config.js`).
- Допустимые значения проверяются в `env.js`: `local`/`stage`/`preprod`/`prod`/`contour`. Если значение не входит в набор — сборка падает с ошибкой.
- `build.config.js` по `BUILD_ENV` выбирает режим webpack (`mode`/`devtool`): `local`/`stage` → `development` + `eval-source-map`, `prod`/`preprod`/`contour` → `production` + `source-map`.
- В рантайме `BUILD_ENV` определяет базовые хосты сервисов (`module/api/hosts.ts`, `resolveHost`) и тип http-сервиса (`module/api/http-service.ts`: при `local``setTypeOfHttpService("original")`). Подробности по хостам и эндпоинтам — в `frontend.ENDPOINTS.md`.
## Переменные сборки
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `BUILD_ENV` | `env.js`, `build.config.js`, `webpack.config.js` (`DefinePlugin`), `Dockerfile` (`ARG`), `.gitlab-ci.yml` (`--build-arg`) | Окружение сборки: `local`/`stage`/`preprod`/`prod`/`contour`. Определяет режим сборки и базовые хосты API |
| `NPM_NEXUS_TOKEN` | `Dockerfile` (`ARG`), `.npmrc`, `.gitlab-ci.yml` (`--build-arg`) | Токен доступа к приватному npm-реестру (Nexus) при `npm i` |
Отдельного `.env`-файла в репозитории нет; переменные передаются как build-arg'и Docker/CI.
## Сборка (`package.json`, `Dockerfile`)
Скрипты npm:
| Скрипт | Команда | Назначение |
| --- | --- | --- |
| `build-module` | `webpack --config webpack.config.js` | Сборка модуля в `dist` (используется в образе) |
| `serve-module` | `webpack serve --config webpack.config.js` | Dev-сервер (порт `9002`, https) |
| `lint` | `eslint ./module/**/*.ts(x) --fix` | Линтинг |
| `start` | `BUILD_ENV=local run-p serve-module storybook` | Локальный запуск (dev-сервер + Storybook) |
| `storybook` / `build-storybook` | `start-storybook` / `build-storybook` | Storybook |
Сборка образа (`Dockerfile`, multi-stage):
1. Стадия `static` (`node:16`): `npm i --legacy-peer-deps` с `NPM_NEXUS_TOKEN`, затем `npm run lint` и `BUILD_ENV=$BUILD_ENV npm run build-module` → артефакты в `/app/dist`.
2. Финальная стадия (`nginx:1.19.6`): копирует `dist` в `/dist` и `nginx/nginx.conf` в `/etc/nginx/nginx.conf`.
Module Federation (`webpack.config.js`, `ModuleFederationPlugin`): имя remote — `srx_documentations`, `filename: module/remoteEntry.js`. Экспонируемые модули: `./DocumentationsPage`, `./DocumentSelect`, `./CreateDocDialog`, `./DownloadFilesDialog`, `./FileBindingsDialog`. Shared-зависимости (singleton): `react`, `react-dom`, `@material-ui/core`, `@material-ui/styles`, `@sarex-team/sdk-js`, `@sarex-team/translator`, `@sarex-team/ui-kit`, `mobx`, `mobx-react-lite`.
## Деплой (Helm, `.helm/values.yaml`)
Чарт использует общий `universal-chart`. Ключевые значения для сервиса `frontend`:
- `deployment.name._default`: `documentation-frontend-static`; порт контейнера `80`.
- `replicaCount`: `stage` — 1, `preprod` — 2, `production` — 2.
- Пробы `liveness`/`readiness`: `httpGet /ping` на порту `80`.
- `resources.requests`: `memory 100Mi`, `cpu 100m`.
- `image.name._default`: `cr.yandex/crp3ccidau046kdj8g9q/documentation-frontend-static:latest` (в CI переопределяется на собранный `IMAGE_NAME` через `HELM_SET_ARGS`).
- `service`: `ClusterIP`, порт `80``stage``8080`), `targetPort 80`, `portName http`.
- `imagePullSecrets.name._default`: `dockerhub`.
## CI/CD (`.gitlab-ci.yml`)
Пайплайн подключает шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). `SERVICE_NAME`: `documentation-frontend-app`. Окружение переключается по ветке/тегу (`workflow.rules`):
| Условие | STAND | Namespace | `BUILD_ENV` | `global.env` | `CHART_VERSION` |
| --- | --- | --- | --- | --- | --- |
| ветка `stage` | `stage` | `documentations` | `stage` | `stage` | `0.0.1-stage` |
| ветка `master` | `preprod` | `documentations-preprod` | `preprod` | `preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `prod` | `production` | `0.0.1-prod` |
| merge request | — | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE: "false"`) |
Каждая ветка задаёт `BUILD_ARGS` (`--build-arg BUILD_ENV=... --build-arg NPM_NEXUS_TOKEN=...`) и `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`), а также `K8S_HUSTLER_BRANCH` соответствующего окружения (`universal-chart-stage`/`-preprod`/`-production`).

View File

@ -0,0 +1,224 @@
# Эндпоинты, с которыми взаимодействует documentation-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `documentation-frontend`, образ `documentation-frontend-app`, деплой `frontend`). Модуль публикуется как remote для Module Federation (`webpack.config.js`, имя `srx_documentations`).
## Как устроено взаимодействие
Все запросы описаны декларативно в реестре `module/api/endpoints.ts` (объект `endpoints`). Каждый эндпоинт задаётся структурой `Endpoint`:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `method` — HTTP-метод (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`);
- `path(args)` — функция, возвращающая путь запроса (с подстановкой параметров/query);
- `body(args)` — опционально, формирование тела запроса;
- `responseType` — опционально, тип ответа (напр. `blob`);
- `accessToken(args)` — опционально, явная передача access-токена в заголовки;
- `cache`, `queryOptions` — опции кеширования/повторов;
- `showErrorNotification` — показывать ли уведомление об ошибке (по умолчанию `true`).
Запрос выполняется единой функцией `fetch(endpoint, params, controller)` (`module/api/endpoints.ts`), которая через `httpService` (`module/api/http-service.ts`, обёртка `createHttpService` из `@sarex-team/sdk-js` поверх `axios`) отправляет запрос на базовый хост сервиса. Базовый хост выбирается по паре «`service` + окружение»: `httpService` создаётся с картой хостов `apiHosts` и текущим `buildEnv`, и разрешает хост внутри себя. Тот же алгоритм продублирован в экспортируемом хелпере `resolveHost(service)` из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта.
Окружение определяется глобальной константой `BUILD_ENV`, которую webpack подставляет в бандл через `DefinePlugin` (`webpack.config.js`) из переменной сборки `process.env.BUILD_ENV`. Допустимые значения проверяются в `env.js`: `local`/`stage`/`preprod`/`prod`/`contour`. В режиме `local` тип http-сервиса переключается на `"original"` (`module/api/http-service.ts`).
Ошибки маппируются в человекочитаемые сообщения в `module/api/errors.ts` (`resolveNetworkErrorByCode`, `resolveNetworkError`).
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts` (объект `apiHosts`). Итоговый URL = `<базовый хост сервиса>` + `path` эндпоинта.
| Сервис (`service`) | Назначение | `local` | `stage` | `preprod` | `prod` | `contour` |
| --- | --- | --- | --- | --- | --- | --- |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `/sarex-backend` | `/` | `/` | `/` | `/` |
| `documentations` | Сервис документации (документы, бандлы, файлы) | `https://stage-api.sarex.io/documentations` | `https://stage-api.sarex.io/documentations` | `https://api.preprod.sarex.io/documentations` | `https://api.sarex.io/documentations` | `/documentations` |
| `sarexApi` | Gateway/API Sarex (`/gateway`, `/eav`, `/cde`, `/transmittals`, `/flows`, `/issues`) | `https://stage-api.sarex.io` | `https://stage-api.sarex.io` | `https://api.preprod.sarex.io` | `https://api.sarex.io` | `/` |
| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://stage-api.sarex.io/workspaces` | `https://api.preprod.sarex.io/workspaces` | `https://api.sarex.io/workspaces` | `/workspaces` |
| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://stage-api.sarex.io/workflows` | `https://api.preprod.sarex.io/workflows` | `https://api.sarex.io/workflows` | `/workflows` |
| `processes` | Сервис рабочих процессов (flows, reviews) | `https://stage-api.sarex.io/flows` | `https://stage-api.sarex.io/flows` | `https://api.preprod.sarex.io/flows` | `https://api.sarex.io/flows` | `/flows` |
| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://stage-api.sarex.io/remarks` | `https://api.preprod.sarex.io/remarks` | `https://api.sarex.io/remarks` | `/remarks` |
| `files` | Сервис файлов | `https://stage-api.sarex.io/files` | `https://stage-api.sarex.io/files` | `https://api.preprod.sarex.io/files` | `https://api.sarex.io/files` | `/files` |
| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` |
| `bim` | BIM-API | `https://stage-bim-api.sarex.io` | `https://stage-bim-api.sarex.io` | `https://bim-api.preprod.sarex.io` | `https://bim-api.sarex.io` | `""` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://idp.dev.stage.sarex.io` | `https://login.preprod.sarex.io` | `https://login.sarex.io` | `""` |
> Хосты `google`, `bim` и `zitadel` заданы в карте хостов, но напрямую в реестре `endpoints` не используются — они задействованы через SDK/вьюер (`@sarex-team/sdk-js`) и механизм аутентификации. В окружении `local` сервис `sarex` проксируется на `/sarex-backend`, в `stage`/`preprod`/`prod` — на `/` (относительные пути), в `contour` все сервисы работают по относительным путям изолированного контура. Отдельного `module-hosts.ts` (карты хостов удалённых модулей) в репозитории нет.
## Эндпоинты по сервисам
### `sarex` — Локальный сервис данных
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getSettings` | GET | `/api/client/settings/` | Клиентские настройки (кешируется, `stateTime: 5`) |
| `getUser` | GET | `/api/core/users/{userId}/` | Пользователь по id |
| `getUsersByCompanyId` | GET | `/api/core/users/?company={companyId}&limit={limit}&offset={offset}` | Пользователи компании (пагинация) |
| `getTargets` | GET | `/api/core/targets/` | Список таргетов |
| `getCompanies` | GET | `/api/core/companies/` | Список компаний |
| `getDepartmentById` | GET | `/api/core/admin/departments?company={companyId}` | Отделы компании |
| `getUsersPositionById` | GET | `/api/core/admin/positions/?company={companyId}` | Должности компании |
| `getMrpaList` | POST | `/api/core/mrpa/list/` | Список МЧД (фильтр по пользователю/компании) |
| `getByFullUrl` | GET | `{url}` | Запрос по произвольному URL |
### `documentations` — Сервис документации
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getDisks` | GET | `/api/v1/disks` | Список дисков |
| `getDocumentTypes` | GET | `/api/v1/documents/types` | Справочник типов документов |
| `getAllPermmission` | GET | `/api/v1/permissions` | Все права доступа |
| `getDocPermission` | GET | `/api/v1/documents/{id}/permissions` | Права доступа документа |
| `postPermission` | POST | `/api/v1/documents/{id}/permissions` | Выдать права сервисному аккаунту |
| `postBundle` | POST | `/api/v1/bundles` | Создать бандл |
| `postFile` | POST | `/api/v1/bundles/{bundleId}/{fileKey}?single_upload=1` | Загрузить файл (single upload) |
| `uploadFileStart` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_multipart` | Начать multipart-загрузку файла |
| `uploadFolderStart` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_multipart?upload_path={folderPath}` | Начать multipart-загрузку с указанием пути |
| `uploadPart` | PUT | `/api/v1/bundles/{bundleId}/{bundleKey}?part_number={partNumber}` | Загрузить часть файла |
| `bundleComplite` | POST | `/api/v1/bundles/{bundleId}/upload_finish` | Завершить загрузку бандла |
| `bundleCompleteUpload` | POST | `/api/v1/bundles/{bundleId}/upload_finish` | Завершить загрузку бандла (дубль ключа) |
| `getFile` | GET | `/api/v1/bundles/{bundleId}/{bundleKey}` | Получить файл бандла |
| `updateComment` | PATCH | `/api/v1/bundles/{bundleId}/comment` | Обновить комментарий бандла |
| `getBundles` | GET | `/api/v1/documents/{id}/bundles` | Бандлы документа |
| `addBundle` | POST | `/api/v1/documents/{documentId}/add_bundle` | Привязать бандл к документу |
| `moveBundles` | PATCH | `/api/v1/documents/{documentId}/move_bundles` | Переместить бандлы |
| `removeBundle` | DELETE | `/api/v1/bundles/{id}` | Удалить бандл |
| `postWorkspace` | POST | `/api/v1/workspaces` | Создать рабочую область |
| `getDocumentById` | GET | `/api/v1/documents/{id}` | Документ по id |
| `getDocumentWithBundles` | GET | `/api/v1/documents/{id}?extend=bundles` | Документ с бандлами |
| `changeDocument` | PATCH | `/api/v1/documents/{documentId}` | Переименовать документ |
| `changeDocumentName` | PATCH | `/api/v1/documents/{id}` | Переименовать документ |
| `updatePath` | PATCH | `/api/v1/documents/{id}/update-path` | Сменить родителя документа |
| `updateDocumentsPaths` | PATCH | `/api/v1/documents/update-path` | Массовая смена родителя |
| `deleteDocument` | DELETE | `/api/v1/documents/{id}` | Удалить документ |
| `deleteDocuments` | DELETE | `/api/v1/documents?document_ids={ids}` | Удалить несколько документов |
| `getFolderChildrenWithActiveProcesses` | POST | `/api/v1/documents/flows` | Дети папки с активными процессами |
| `downloadFile` | GET | `/api/v1/bundles/{lastBundleId}/{key}/download` | Скачать файл (с флагами `include_original_pdf`/`include_printable_pdf`) |
| `downloadFiles` | GET | `/api/v1/download_url/documents?document_ids={documentIds}` | Получить ссылку на скачивание документов |
| `downloadAllFiles` | GET | `/api/v1/bundles/{lastBundleId}/download` | Скачать все файлы бандла |
| `downloadFolder` | GET | `/api/v1/documents/{docId}/download?depth={depth}` | Скачать папку |
| `getFoldersDownloadUrl` | GET | `/api/v1/documents/get_folders_download_url?document_ids={ids}` | Ссылка на скачивание папок |
| `conversionFile` | POST | `/api/v1/conversion` | Конвертация документа (в IFC) |
| `addMarks` | PUT | `/api/v1/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи |
| `sign` | POST | `/api/v1/bundles/{bundleId}/sign` | Подписать бандл |
| `cancelQrCode` | PATCH | `/api/v1/bundles/{bundleId}/cancel_qr` | Отменить QR-код |
| `restartWorkflow` | POST | `/api/v1/bundles/{bundleId}/restart` | Перезапустить workflow бандла |
| `getPublicLink` | GET | `/api/v1/public/documents/public_link/{public_link_id}` | Получить публичную ссылку |
| `createPublicLink` | POST | `/api/v1/documents/public_link` | Создать публичную ссылку |
| `updatePublicLink` | PATCH | `/api/v1/documents/public_link/{public_link_id}` | Обновить публичную ссылку |
| `deletePublicLink` | DELETE | `/api/v1/documents/public_link/{public_link_id}` | Удалить публичную ссылку |
| `removeDoc` | DELETE | `/api/v1/documents/bin?parent_id={id}` | Переместить в корзину |
| `recoveryDocument` | PATCH | `/api/v1/documents/bin/restore?parent_id={id}` | Восстановить из корзины |
| `copyFolderStructure` | POST | `/api/v1/documents/copy_structure` | Копировать структуру папок |
| `getTemplateJSON` | GET | `/api/v1/templates/{bundleId}` | JSON-шаблон бандла |
| `uploadSingleTemplate` | POST | `/api/v1/bundles/{bundleId}/{key}/upload_single` | Загрузить файл шаблона |
| `createReport` | POST | `/api/v1/documents/create_report` | Сформировать отчёт по документам |
| `updateChangelog` | PATCH | `api/v1/changelogs/{bundleId}` | Обновить запись журнала изменений |
| `createChangelog` | POST | `api/v1/changelogs/create` | Создать запись журнала изменений |
| `getFavorites` | GET | `/api/v1/favorite_documents?company_id={companyId}` | Избранные документы |
| `createFavoriteDocument` | POST | `/api/v1/favorite_documents` | Добавить документ в избранное |
| `deleteFavoriteDocument` | DELETE | `/api/v1/favorite_documents/{documentId}` | Убрать документ из избранного |
| `getNearestNameTemplate` | GET | `/api/v1/documents/{documentId}/name_template` | Ближайший шаблон именования (без уведомления об ошибке) |
| `getDocumentNameTemplate` | GET | `/api/v1/name_templates/{documentId}` | Шаблон именования документа (без уведомления об ошибке) |
| `createNameTemplate` | POST | `/api/v1/name_templates/create` | Создать шаблон именования |
| `updateNameTemplate` | PATCH | `/api/v1/name_templates/{documentId}` | Обновить шаблон именования |
| `deleteNameTemplate` | DELETE | `/api/v1/name_templates/{documentId}` | Удалить шаблон именования |
| `createLink` | POST | `/api/v1/links` | Создать ярлык (ссылку на документ) |
| `getBundleMrpas` | GET | `/api/v1/bundles/{bundleId}/mrpas` | МЧД бандла |
### `sarexApi` — Gateway/API Sarex
Через этот сервис проходят запросы к `/gateway`, `/eav`, `/cde`, а также к сабпутям других доменов, доступным через общий шлюз: `/transmittals`, `/flows`, `/issues`.
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getDocuments` | GET | `/gateway/api/v1/disks/{id}/documents?parent_id={parentId}&child_id={childId}` | Документы диска (по родителю/потомку) |
| `getFolderChildren` | GET | `/gateway/api/v1/disks/{diskId}/documents?parent_id={documentId}` | Дети папки |
| `getSearch` | GET | `/gateway/api/v4/disks/{diskId}/documents?...` | Поиск/фильтрация документов (root_document, limit, filters, bookmark) |
| `createDocument` | POST | `/gateway/api/v1/documents` | Создать документ/папку/проект |
| `fetchDocumentPaths` | POST | `/gateway/api/v1/documents/ancestors` | Предки документов |
| `getAttributesByDocument` | GET | `/gateway/api/v1/documents/{id}/attributes` | Атрибуты документа |
| `updateAttributes` | PUT | `/gateway/api/v1/documents/{id}/attributes` | Обновить атрибуты документа |
| `addAttributes` | POST | `/gateway/eav/api/v0/entity/` | Создать сущность атрибутов (EAV) |
| `getDefaultAttributes` | GET | `/eav/api/v0/schema/?model=document&company_id={companyId}&type_identifier={docType}` | Схема атрибутов по типу |
| `getAttributes` | GET | `/eav/api/v0/attribute/?company_id={params}` | Атрибуты компании |
| `getAttributesWithParams` | GET | `/eav/api/v0/schema/?model=document&{params}` | Схема атрибутов с параметрами |
| `updateSubscription` | POST | `/gateway/api/v1/subscription/` | Создать/обновить подписку |
| `deleteSubscription` | DELETE | `/gateway/api/v1/documents/{documentId}/subscription/` | Удалить подписку |
| `getActivityLog` | GET | `/gateway/api/v1/system_log/?model_names=document&...` | Журнал активности документа |
| `fetchResourceByDocumentId` | GET | `/gateway/api/v1/resources-rpc/resource-by-document-id/{id}/` | Ресурс по id документа |
| `getUsersWithTransmittalProjectPermissions` | GET | `/gateway/api/v2/users/?limit=5000&offset=0&resource_id={projectId}&permissions={permissions}` | Пользователи с правами в проекте |
| `getRemovedDocuments` | GET | `/gateway/api/v1/documents/bin?parent_id={id}{params}` | Удалённые документы в папке |
| `getRemovedFilteredDocuments` | GET | `/gateway/api/v1/documents/bin{params}` | Удалённые документы (фильтр) |
| `getTemplates` | GET | `/gateway/api/v1/disks/{diskId}/flat_documents?type={type}` | Плоский список документов по типу |
| `getFileSize` | GET | `/gateway/api/v1/documents/size?disk_id={diskId}&document_id={documentId}` | Размер документа |
| `completeUpload` | POST | `{uploadUrl}/complete` | Завершение загрузки (по переданному URL) |
| `createTransmittal` | POST | `/transmittals/api/v1/transmittals/create` | Создать трансмиттал |
| `getTransmittalById` | GET | `/transmittals/api/v1/transmittals/{transmittalId}` | Трансмиттал по id |
| `getTransmittalsByBundleId` | POST | `/transmittals/internal/v1/transmittals/by_bundle_ids` | Трансмитталы по id бандлов |
| `getTemplateListForSelect` | GET | `/transmittals/api/v1/transmittal_templates/select?resource={resourceId}` | Список шаблонов трансмитталов для выбора |
| `getSingleTemplate` | GET | `/transmittals/api/v1/transmittal_templates/{templateId}?resource={resourceId}` | Шаблон трансмиттала по id |
| `createPrescriptionDocument` | POST | `/issues/api/prescriptions/{prescriptionId}/generate/` | Сгенерировать документ по предписанию |
| `loadReviewData` | GET | `/flows/api/v1/documents/?bundle_ids={ids}&limit=10000&full=true` | Данные согласований по бандлам (с явным access-токеном) |
| `searchAssets` | POST | `eav/api/v4/assets/search/` | Поиск активов по id |
| `getProjectAssets` | GET | `eav/api/v4/assets/?linkable_to_project={resourceId}&parentId=null` | Активы проекта |
| `getAssetsList` | GET | `eav/api/v4/assets/?tenant_id={tenantId}&linkable_to_project={resourceId}&depth=0&...` | Список активов (поиск/пагинация) |
| `getAssets` | GET | `eav/api/v4/assets/?{params}` | Активы по произвольным параметрам |
| `getAssetLevels` | GET | `eav/api/v4/assets/?tenant_id={tenantId}&parent_id={parentAssetId}&path_contains={id}` | Уровни активов |
| `getBindings` | GET | `/cde/app/v1/bundles/{bundleId}/bindings` | Привязки бандла |
| `createSession` | POST | `/cde/app/v1/s32d/sessions/` | Создать S32D-сессию |
| `getSessionList` | GET | `/cde/app/v1/s32d/sessions/` | Список S32D-сессий |
| `uploadS32DSingle` | PUT | `/cde/app/v1/s32d/sessions/{sessionId}/archive` | Загрузить архив S32D (single) |
| `s32dMultipartInit` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart` | Начать multipart-загрузку архива S32D |
| `s32dMultipartUploadPart` | PUT | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart/{uploadId}/parts/{partNumber}` | Загрузить часть архива S32D |
| `s32dMultipartComplete` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/archive/multipart/{uploadId}/complete` | Завершить multipart-загрузку S32D |
| `startS32DProcess` | POST | `/cde/app/v1/s32d/sessions/{sessionId}/process` | Запустить обработку S32D |
### `processes` — Сервис рабочих процессов (flows)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getProcesses` | GET | `/api/v1/flows/?{query}` | Список процессов (flows) |
| `createReview` | POST | `/api/v1/reviews/` | Создать review |
| `deleteReview` | DELETE | `/api/v1/reviews/{id}/` | Удалить review |
| `activateReview` | PATCH | `/api/v1/reviews/{id}/approve/` | Активировать/утвердить review |
| `createReviewDocuments` | POST | `/api/v1/documents/` | Добавить документы в review |
### `workflows` — Сервис обработки документов
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getWorkflow` | GET | `/api/v1/workflows/{workflowId}` | Workflow по id |
| `getWorkflows` | POST | `/api/v1/workflows/batch` | Пакетное получение workflow |
### `workspaces` — Сервис рабочих областей
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getWorkspaces` | GET | `/api/v1/workspaces/{uuid}` | Рабочая область по uuid |
### `remarks` — Сервис замечаний
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getRemarksTotalCount` | GET | `/api/v1/total_count` | Общее число замечаний |
### `files` — Сервис файлов
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `downloadBundlesMrpas` | POST | `/api/v1/bundles_mrpas/` | Скачать МЧД бандлов (ответ `blob`) |
## Обработка ошибок
Маппинг ошибок выполняется в `module/api/errors.ts`. Функция `fetch` (`module/api/endpoints.ts`) перехватывает `AxiosError` и вызывает:
- `resolveNetworkErrorByCode(service, code, endpoint, method, data)` — при HTTP-ответе с кодом ≥ 400 и при отмене запроса (`CanceledError`/`ERR_CANCELED`, внутренний код `-1` → «Запрос был отменен»);
- `resolveNetworkError(service, error)` — при сетевой ошибке без ответа сервера.
Базовый маппинг кодов (`httpCodeToError`): `400` — «некорректный формат запроса», `401` — «ошибка авторизации», `403` — «доступ запрещен», `404` — «ресурс не найден», `500` — «ошибка сервера». Для неизвестного кода подбирается ближайший (`4xx` → `400`, иначе → `500`). К сообщению добавляется человекочитаемое имя сервиса из `getServiceToName()` (напр. `documentations`/`sarexApi` → «Сервис документации», `sarex` → «Локальный сервис данных», `processes` → «Сервис рабочих процессов», `workflows` → «Сервис обработки документов», `remarks` → «Сервис замечаний», `workspaces` → «Сервис рабочих областей», `files` → «Сервис файлов»).
Для части кодов сообщение уточняется по эндпоинту и методу:
- `401` — набор сообщений `error401Messages` (истёкшая/невалидная сессия, завершённая сессия); по умолчанию — «Ваш токен невалиден, обновите страницу».
- `403` — тип определяется `determine403ErrorType(endpoint, method)` (напр. чтение/создание/редактирование/удаление/перемещение документа, скачивание, загрузка файла, управление доступом, изменение атрибутов, создание review/трансмиттала, доступ к диску/проекту), сообщения — `error403Messages`.
- `400` — тип определяется `determine400ErrorType(endpoint, method, data)` с анализом текста `data.message` (конфликт имени, дубликат, отсутствие/некорректность расширения, некорректный формат), сообщения — `error400Messages`.
- `409` — тип определяется `determine409ErrorType(endpoint, method, data)` (дубликаты имён документов/папок/ярлыков, конфликт при `copy_structure`), сообщения — `error409Messages`.
По умолчанию у запросов включён показ уведомления об ошибке (`showErrorNotification !== false`); отдельные эндпоинты отключают его (`getNearestNameTemplate`, `getDocumentNameTemplate`). Типы кодов ошибок описаны в `module/api/types.ts`.

View File

@ -0,0 +1,241 @@
# Конфигурация проекта pdm
Документ описывает все переменные окружения и способы конфигурирования сервиса **pdm** (Go). Сервис деплоится в namespace `documentations` как деплоймент `pdm` (образ `pdmv2`). Это шлюз/агрегатор поверх Postgres и множества внутренних сервисов Sarex (документации, ресурсы, ремарки, вложения, состояния, подписки, EAV, инспекции, релизы, BIM, трансмитталы и др.).
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` (функция `NewConfig`) через библиотеку [`cleanenv`](https://github.com/ilyakaznacheev/cleanenv) — вызовом `cleanenv.ReadEnv(cfg)`.
Особенности разбора:
- **Плоские имена переменных без общего префикса** — каждое поле помечено тегом `env:"..."` (напр. `POSTGRES_ADDRESS`, `RESOURCES_URL`). Вложенности/делимитера, как в pydantic-settings, здесь нет.
- **Обязательность** задаётся тегом `env-required:"true"` — при отсутствии такой переменной приложение не стартует (`config error`). В таблицах ниже дефолт `—` означает обязательное поле.
- **Значения по умолчанию** задаются тегом `env-default:"..."`.
- `cleanenv.ReadEnv` читает **только переменные окружения процесса** — конфиг-файла (yaml/toml) и авто-загрузки `.env` нет. Единственный файловый источник — JSON сервисного аккаунта S3 (`S3_SERVICE_ACCOUNT`).
Отдельно от env читается JSON-файл доступа к S3 — путь берётся из `S3_SERVICE_ACCOUNT`, разбор в `pkg/s3` (`NewFromConfigFile`). Формат файла (`.example.s3config.json`):
```json
{ "endpoint": "", "access_key_id": "", "secret_access_key": "", "use_ssl": true }
```
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. `make config` копирует `.example.env``.env` и `.example.s3config.json``.s3config.json` (только если файлов ещё нет), но приложение **не загружает `.env` автоматически**его нужно экспортировать самому. В репозитории для этого есть `.envrc` (`use flake` + `dotenv`) под direnv |
| Локально (live-reload) | `make run-dev``air` (`.air.toml`), пересборка `./cmd/httpserver/main.go` |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `services.api.envs` (обычные значения, ключ `_default` и переопределения по окружениям `stage`/`preprod`/`production`) и `services.api.secretEnvs` (значения из k8s-секретов через `secretKeyRef`). Используется зонтичный `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна в `workflow.rules` (по ветке/тегу), общие шаблоны из `generic/common-ci` |
Точки входа (`cmd/`):
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `make run` / `make build` | `cmd/httpserver/main.go` | HTTP API (Fiber). Читает конфиг и вызывает `internal/app/http.New(cfg).Run()` |
| — | `cmd/example/main.go`, `cmd/test/main.go` | Вспомогательные утилиты (не участвуют в деплое) |
Порядок инициализации в `internal/app/http/httpserver.go` (`App.Run`): трейсер (при `TRACER_USE=true`) → подключение к Postgres → инициализация HTTP-клиентов внешних сервисов → репозитории/usecase/сервисы → опциональный Valkey → сборка Fiber-приложения (`internal/controller/http/v1.Setup`) → `app.Listen(":8080")`.
## Переменные приложения
Все переменные читаются `config/config.go`. Дефолт `—` означает, что значение обязательно (`env-required:"true"`) и его отсутствие приводит к ошибке старта.
### App / Log
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `APP_NAME` | string | — | Имя приложения |
| `APP_VERSION` | string | — | Версия приложения |
| `LOG_LEVEL` | string | — | Уровень логирования (`pkg/logging`, напр. `DEBUG`/`INFO`) |
### Postgres (`POSTGRES`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL |
| `POSTGRES_DB` | string | — | Имя базы данных |
| `POSTGRES_USER` | string | — | Пользователь БД |
| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД |
| `POSTGRES_PORT` | string | — | Порт PostgreSQL |
| `POSTGRES_POOL_SIZE` | int32 | — | Размер пула соединений (pgxpool) |
| `ENABLE_OBSERVABILITY` | bool | `false` | Инструментирование пула Postgres трейсингом (`otelpgx`) |
> DSN собирается в `Config.GetPostgresConnectionUrl()` как `postgres://user:password@address:port/db`**без параметра `sslmode`**. Отдельный флаг `ENABLE_SSL` из Helm кодом не читается (см. «Замечания»).
### HTTP (`HTTP`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HTTP_PORT` | string | — | Порт HTTP. **Обязателен по тегу, но фактически не используется** — сервер слушает `:8080` (хардкод в `httpserver.go`) |
| `PUBLIC_KEY` | string (PEM) | `""` | Публичный ключ (PKIX) для проверки JWT. Формально необязателен, но при пустом/некорректном значении приложение падает (`panic` в `v1.Setup`) |
| `HTTP_BODY_LIMIT` | int | `268435456` (256 MB) | Максимальный размер тела запроса, байт |
| `HTTP_READ_BUFFER_SIZE` | int | `98304` (96 KB) | Размер буфера чтения запроса, байт |
### Auth и хосты внешних сервисов
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_BASIC_AUTH` | string | — | Basic-токен для авторизации в бэкенде Sarex/Django (клиенты `targets`, `users`) |
| `DJANGO_HOST` | string | — | Базовый URL Django/бэкенда. Используется сразу двумя секциями — `USERS.UserHost` и `SA.SAHost` (accounts/companies/django-клиенты) |
| `NOTES_URL` | string | — | Сервис заметок (`notes`) |
| `FLOWS_URL` | string | — | Сервис процессов (`flows`) |
| `RESOURCES_URL` | string | — | Сервис ресурсов/IAM (`resources`) |
| `REMARKS_URL` | string | — | Сервис замечаний (`remarks`) |
| `ATTACHMENTS_URL` | string | — | Сервис вложений (`attachments`) |
| `STATES_URL` | string | — | Сервис состояний/рабочих областей (`workspaces`) |
| `SUBSCRIPTIONS_URL` | string | — | Сервис подписок (`subscriptions`) |
| `EAV_URL` | string | — | Сервис атрибутов EAV |
| `INSPECTIONS_URL` | string | — | Сервис инспекций |
| `SYSTEM_LOG_URL` | string | — | Сервис системного лога |
| `TARGET_URL` | string | — | Сервис таргетов |
| `DOCUMENTATION_URL` | string | — | Сервис документаций (documentation-api-v2) |
| `BIM_V2_HOST` | string | — | BIM core API v2 |
| `NOTES_URL` | string | — | (см. выше) |
| `DRAWINGS_INTERNAL_URL` | string | — | Внутренний URL сервиса чертежей |
| `RELEASES_URL` | string | — | URL GitLab для получения релизов |
| `RELEASES_TOKEN` | string | — | Токен доступа к GitLab (`RELEASES_URL`) |
### Ресурсы и фильтр разрешений (`RESOURCES`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_PERMISSIONS_FILTER` | bool | `false` | Включить фильтрацию по разрешениям на уровне сервиса документов |
| `PERMISSIONS_FILTER_COMPANIES` | string (JSON-массив) | `[133, 256, 247, 219, 248, 194, 242, 260, 252, 255, 239, 125, 116, 92, 311, 170]` | Список ID компаний, к которым применяется фильтр. Парсится `json.Unmarshal` в `[]uint64` |
### Thumbnails (`ATTACHMENTS`, `STATES`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WIDTH_THUMB_ATTACHMENTS` | int | `100` | Ширина превью вложений |
| `HEIGHT_THUMB_ATTACHMENTS` | int | `100` | Высота превью вложений |
| `WIDTH_THUMB_STATES` | int | `100` | Ширина превью состояний |
| `HEIGHT_THUMB_STATES` | int | `100` | Высота превью состояний |
### Subscriptions / System log — доп. поля
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_SUBSCRIPTIONS` | bool | `true` | Включить интеграцию с подписками в сервисе документов |
| `API_HOST_PREFIX` | string | `""` | Префикс хоста API (напр. `/gateway`), используется сервисом системного лога |
### S3 (`S3`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_SERVICE_ACCOUNT` | string | — | Путь к JSON-файлу с доступом к S3 (`endpoint`, `access_key_id`, `secret_access_key`, `use_ssl`). Разбирается в `pkg/s3` |
### Transmittals (`Transmittals`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRANSMITTALS_ENABLE` | bool | `true` | Включить клиент трансмитталов; при `false` используется stub-реализация |
| `TRANSMITTALS_BASE_URL` | string | — | Базовый URL сервиса трансмитталов. Обязателен даже при `TRANSMITTALS_ENABLE=false` |
### Observability / Tracer
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `OBSERVABILITY_COLLECTOR_ENDPOINT` | string | `""` | Эндпоинт OTLP-коллектора (метрики/наблюдаемость) |
| `TRACER_USE` | bool | `false` | Включить трейсинг OpenTelemetry (`golang-fiber-otel-tools`) |
| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора трейсов |
| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору |
| `SERVICE_NAME` | string | `Pdm` | Имя сервиса в трейсах |
| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя логгера OTel |
### Valkey (`VALKEY`) — кэш пользователей
Подключение опционально: если `VALKEY_ADDR` пуст — клиент не создаётся (кэш пользователей отключён). Ошибки подключения/пинга не фатальны (лог `Warn`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `VALKEY_ADDR` | string | `""` | Адрес Valkey (при пустом — кэш выключен) |
| `VALKEY_LOGIN` | string | `""` | Логин |
| `VALKEY_HOST` | string | `""` | Хост |
| `VALKEY_PASSWORD` | string | `""` | Пароль |
| `VALKEY_DB` | int | `0` | Номер БД |
| `VALKEY_SSL` | bool | `false` | Использовать TLS |
| `VALKEY_SSL_CA_CERTS` | string | `""` | Путь к CA-сертификату для TLS |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения (`config/config.go`), но участвуют в сборке/деплое:
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `GITLAB_CREDENTIALS` | `Dockerfile` (build-arg), `.gitlab-ci.yml` (`BUILD_ARGS`) | Учётные данные для доступа к приватным Go-модулям `gitlab.sarex.io` при сборке |
| `SERVICE_NAME` (CI) | `.gitlab-ci.yml` | `pdmv2` — имя сервиса в пайплайне (не путать с `SERVICE_NAME` трейсера) |
| `DOCKERFILE_PATH`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` | Путь к Dockerfile, источник триггера |
| `ENABLE_LINTER`, `ENABLE_BUILD_CHART`, `ENABLE_BUILD_IMAGE`, `ENABLE_STATE_UPDATE`, `ENABLE_DEPLOY` | `.gitlab-ci.yml` | Флаги стадий пайплайна |
| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME` | `.gitlab-ci.yml` | Параметры окружения/деплоя Helm |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Обычные значения задаются в `services.api.envs` (ключ `_default` + переопределения по `stage`/`preprod`/`production`) и содержат переменные приложения, описанные выше, различаясь адресами БД/сервисов, `LOG_LEVEL`, доменами и т.п.
Значения из секретов (`services.api.secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`, prod/по умолчанию) | Ключ (`secretKey`) |
| --- | --- | --- |
| `POSTGRES_DB` | `documentations-postgresql-secret` (preprod: `ya-pg-secret`) | `database` |
| `POSTGRES_PORT` | `documentations-postgresql-secret` | `port` |
| `POSTGRES_ADDRESS` | `documentations-postgresql-secret` | `host` |
| `POSTGRES_USER` | `documentations-postgresql-secret` | `username` |
| `POSTGRES_PASSWORD` | `documentations-postgresql-secret` | `password` |
| `YC-PG-CERTIFICATE` | `documentations-postgresql-secret` (preprod: `yc-pg-certificate`) | `ca.crt` |
| `DJANGO_BASIC_AUTH` | `django-auth` | `key` |
| `PUBLIC_KEY` | `public-key` | `key` |
| `RELEASES_TOKEN` | `releases-token` | `key` |
| `VALKEY_ADDR` | `valkey-secret` | `url` |
| `VALKEY_LOGIN` | `valkey-secret` | `login` |
| `VALKEY_PASSWORD` | `valkey-secret` | `password` |
| `VALKEY_HOST` | `valkey-secret` | `host` |
| `VALKEY_PORT` | `valkey-secret` | `port` |
| `VALKEY_CA_CERTS` | `valkey-secret` | `cert` |
Помимо env, чарт монтирует секрет `documentations-yc-s3` как том в `/etc/sarex/yc-s3-storage` (readOnly). Именно на файл `/etc/sarex/yc-s3-storage/yc-s3-service-account.json` указывает `S3_SERVICE_ACCOUNT` в prod-конфигурации.
Прочие значения чарта (не переменные приложения): `deployment.*` (имя `pdm-api`, реплики `stage=3`/`preprod=2`/`production=8`, ресурсы `cpu=1`, `memory=2Gi`), `image.name` (`cr.yandex/.../pdm_v2`), `service.*` (порт `8080`), `imagePullSecrets` (`dockerhub`), `probes.*` (startup/liveness/readiness по `/internal/healthz/*` на порту `8080`).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | `universal-chart.global.env` |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `documentations` | `stage` |
| ветка `master` | `preprod` | `documentations-preprod` | `preprod` |
| тег (`CI_COMMIT_TAG`) | `prod` | `documentations-prod` | `production` |
| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Общие переменные: `SERVICE_NAME=pdmv2`, `RELEASE_NAME=pdmv2`, `CHART_NAME=pdmv2`, `CHART_VERSION=0.0.1-<env>`, `DOCKERFILE_PATH=Dockerfile`.
## Замечания и потенциальные проблемы
- **`HTTP_PORT` фактически игнорируется.** Поле обязательно (`env-required`), но сервер жёстко слушает `:8080` (`app.Listen(":8080")` в `httpserver.go`). Реальный порт задаётся только этим хардкодом; в Helm `HTTP_PORT` и `service.port` совпадают со `8080`, поэтому расхождение незаметно.
- **`PUBLIC_KEY` де-факто обязателен.** По тегам он необязателен (`env:"PUBLIC_KEY"`), но `v1.Setup` при пустом/битом PEM вызывает `panic` (`failed to parse PEM block...`). Для локального запуска нужен валидный публичный ключ.
- **SSL к Postgres в коде не настраивается.** DSN формируется без `sslmode` (`GetPostgresConnectionUrl`). Helm-переменные `ENABLE_SSL` и секрет `YC-PG-CERTIFICATE` кодом **не читаются** — подключение к БД идёт без TLS-параметров на уровне DSN.
- **Множество Helm-переменных не читается приложением.** В `services.api.envs`/`secretEnvs` присутствуют переменные, отсутствующие в `config/config.go`, — вероятно, унаследованы от `documentation-api`: `API_ADDRESS`, `API_ADDRESS_FILE`, `ENABLE_SSL`, `ENABLE_S3`, `FILE_URL_EXTERNAL`, `WORKFLOW_URL`, `WORKSPACE_URL`, `BIM_API_URL`, `BIM_API_V2_URL`, `BIM_API_URL_EXTERNAL`, `WORKSPACE_BUNDLE_VERSION`, `WORKFLOW_IMAGES_VERSION`/`WORKFLOWS_IMAGES_VERSION`, `NAMESPACE`, `DJANGO_ORIGINATOR`, `USE_EXPERIMENTAL`, `READ_WRITE_TIMEOUT_FILE_STREAM`, `CACHE_DEFAULT_EXPIRATION`, `CACHE_CLEANUP_INTERVAL`, `USE_CACHE_IN_FILE_STREAMER`, `SENTRY_DSN`, `SENTRY_DEBUG`, `ENVIRONMENT`, `YC-PG-CERTIFICATE`. Они не влияют на работу pdm.
- **Несовпадение имён Valkey.** Код ждёт `VALKEY_SSL_CA_CERTS` (`config.go`), а Helm-секрет прокидывает `VALKEY_CA_CERTS`; также Helm задаёт `VALKEY_PORT`, который код не читает (адрес берётся целиком из `VALKEY_ADDR`). В результате CA-сертификат и порт из секрета до приложения не доходят.
- **Секции `USERS`/`SA` используют один и тот же env `DJANGO_HOST`.** Оба поля (`UserHost`, `SAHost`) читают одну переменную.
- **`config.env` в репозитории содержит реальные учётные данные** (пароль Postgres, `DJANGO_BASIC_AUTH`, токены) — это конфигурация для отладки, не шаблон. Для примеров использовать `.example.env`; `config.env` не должен попадать в окружения и подлежит ротации секретов.
- **`ENABLE_SQL_QUERY` из `config.env` кодом не читается** — в `config/config.go` такого поля нет.
- **`cleanenv.ReadEnv` не загружает `.env` автоматически.** `make config` лишь создаёт файлы-шаблоны (`.env`, `.s3config.json`) при их отсутствии; переменные нужно экспортировать вручную (например, через `direnv`/`.envrc`).
- **v0-роутер (echo) не подключён.** В `cmd/httpserver/main.go` используется только `internal/controller/http/v1.Setup` (Fiber). Пакет `internal/controller/http/v0` (на `labstack/echo`) в рантайме не задействован.
## Минимальный набор для локального запуска
Приложение поднимается через `make run` (или `make run-dev` с `air`). Минимально необходимо задать (обязательные поля `config/config.go`):
- `APP_NAME`, `APP_VERSION`, `LOG_LEVEL`;
- `POSTGRES_ADDRESS`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_PORT`, `POSTGRES_POOL_SIZE`;
- `HTTP_PORT` (любой — фактически используется `:8080`), а также **валидный** `PUBLIC_KEY` (иначе `panic`);
- `DJANGO_BASIC_AUTH`, `DJANGO_HOST`;
- хосты внешних сервисов: `NOTES_URL`, `FLOWS_URL`, `RESOURCES_URL`, `REMARKS_URL`, `ATTACHMENTS_URL`, `STATES_URL`, `SUBSCRIPTIONS_URL`, `EAV_URL`, `INSPECTIONS_URL`, `SYSTEM_LOG_URL`, `TARGET_URL`, `DOCUMENTATION_URL`, `BIM_V2_HOST`, `DRAWINGS_INTERNAL_URL`;
- `RELEASES_URL`, `RELEASES_TOKEN`;
- `S3_SERVICE_ACCOUNT` (путь к `.s3config.json`) и заполненный сам JSON-файл;
- `TRANSMITTALS_BASE_URL` (обязателен даже при выключенных трансмитталах).
Необязательные (есть дефолты): `ENABLE_OBSERVABILITY`, `HTTP_BODY_LIMIT`, `HTTP_READ_BUFFER_SIZE`, `ENABLE_PERMISSIONS_FILTER`, `PERMISSIONS_FILTER_COMPANIES`, `USE_SUBSCRIPTIONS`, `WIDTH_THUMB_*`/`HEIGHT_THUMB_*`, `API_HOST_PREFIX`, `TRANSMITTALS_ENABLE`, `TRACER_*`, `SERVICE_NAME`, `VALKEY_*`, `OBSERVABILITY_COLLECTOR_ENDPOINT`.
Готовые значения-примеры приведены в `pdm.env.example` (на основе `.example.env` репозитория).

View File

@ -0,0 +1,116 @@
# Пример переменных окружения сервиса pdm (документируемый образ pdmv2).
# Основан на .example.env репозитория; переменные читаются config/config.go
# библиотекой cleanenv (cleanenv.ReadEnv). Значения-заглушки, замените своими.
# Файл автоматически НЕ загружается приложением — переменные нужно экспортировать
# в окружение процесса (напр. через direnv/.envrc или `set -a && . ./.env`).
# App
APP_NAME=pdm
APP_VERSION=0.1.0
# Logger
LOG_LEVEL=DEBUG
# Postgres
POSTGRES_ADDRESS=127.0.0.1
POSTGRES_DB=documentations
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PORT=5432
POSTGRES_POOL_SIZE=10
ENABLE_OBSERVABILITY=false
# Http
# ВНИМАНИЕ: HTTP_PORT читается как обязательный, но сервер всё равно слушает :8080 (хардкод).
HTTP_PORT=8001
# PUBLIC_KEY — публичный ключ в формате PEM (PKIX) для проверки JWT.
# Формально не обязателен по тегам, но при пустом/некорректном значении приложение падает (panic).
PUBLIC_KEY=
HTTP_BODY_LIMIT=268435456
HTTP_READ_BUFFER_SIZE=98304
# Auth (Basic-токен для походов в Django/бэкенд Sarex)
DJANGO_BASIC_AUTH=
# Users / service accounts (один и тот же хост используется как DJANGO_HOST)
DJANGO_HOST=https://stage.sarex.io
# Notes
NOTES_URL=https://stage-api.sarex.io/notes
# Flows
FLOWS_URL=https://stage-api.sarex.io/flows
# Resources
RESOURCES_URL=http://localhost:9000
ENABLE_PERMISSIONS_FILTER=false
PERMISSIONS_FILTER_COMPANIES=
# Remarks
REMARKS_URL=https://stage-api.sarex.io/remarks
# Attachments
ATTACHMENTS_URL=http://localhost:8000
WIDTH_THUMB_ATTACHMENTS=100
HEIGHT_THUMB_ATTACHMENTS=100
# States (workspaces)
STATES_URL=https://stage-api.sarex.io/workspaces
WIDTH_THUMB_STATES=100
HEIGHT_THUMB_STATES=100
# Subscriptions
USE_SUBSCRIPTIONS=true
SUBSCRIPTIONS_URL=https://stage-api.sarex.io/subscriptions
# Eav
EAV_URL=http://stage-api.sarex.io/eav
# Inspections
INSPECTIONS_URL=https://stage-api.sarex.io/inspections
# System log
SYSTEM_LOG_URL=http://localhost:8888
API_HOST_PREFIX=/gateway
# Target
TARGET_URL=https://stage.sarex.io
# Documentation api
DOCUMENTATION_URL=http://localhost:6666/
# Bim v2
BIM_V2_HOST=http://localhost:8888/
# Observability (OTLP-коллектор)
OBSERVABILITY_COLLECTOR_ENDPOINT=
# Releases (GitLab)
RELEASES_URL=https://gitlab.com
RELEASES_TOKEN=
# Drawings
DRAWINGS_INTERNAL_URL=http://localhost:6666
# S3 (путь к JSON-файлу с сервисным аккаунтом, см. .example.s3config.json)
S3_SERVICE_ACCOUNT=.s3config.json
# Transmittals
TRANSMITTALS_ENABLE=true
TRANSMITTALS_BASE_URL=http://transmittal-service.transmittal-api-stage
# Tracer (OpenTelemetry)
TRACER_USE=false
TRACER_HOST=localhost:4317
TRACER_USE_INSECURE=true
SERVICE_NAME=Pdm
TRACER_LOGGER_NAME=tracer_logger
# Valkey (кэш пользователей, опционально; при пустом VALKEY_ADDR не подключается)
VALKEY_ADDR=
VALKEY_LOGIN=
VALKEY_HOST=
VALKEY_PASSWORD=
VALKEY_DB=0
VALKEY_SSL=false
VALKEY_SSL_CA_CERTS=

View File

@ -0,0 +1,918 @@
openapi: 3.0.3
info:
title: PDM API
version: "0.1.0"
description: |
REST API сервиса **pdm** (`pdm/pdm`, образ `pdmv2`) — шлюз/агрегатор над
Postgres и множеством внутренних сервисов Sarex: документации, ресурсы и
разрешения, замечания, вложения, состояния/рабочие области, подписки,
атрибуты (EAV), инспекции, релизы, заметки, чертежи, BIM, трансмитталы,
системный лог.
Сервис написан на Go (**Fiber v2**). Приложение собирается в
`internal/controller/http/v1.Setup` (`internal/controller/http/v1/router.go`),
точка входа — `cmd/httpserver/main.go`. Роутинг делится на группы:
- публичный API — префикс `/api` с версиями `v1`/`v2`/`v3`/`v4`
(`group.Group("v1")` и т.д.);
- внутренний API — префикс `/internal` (healthcheck, pprof, служебные
ручки), без аутентификации на уровне приложения.
Готовой спецификации (swagger) в репозитории нет — данный документ
восстановлен из роутеров `internal/controller/http/**/router.go`.
Сервер слушает порт `8080` (хардкод в `internal/app/http/httpserver.go`).
### Аутентификация
Аутентификация применяется middleware `pkg/httpserver/middleware/auth.go` ко
всем путям с префиксом `/api`. Токен передаётся заголовком
`Authorization: Bearer <jwt>`. Поддерживаются два режима:
1. **sarex-backend** (по умолчанию) — если заголовка `Identity` нет, подпись
основного JWT проверяется публичным ключом из `PUBLIC_KEY` (PKIX). Из
claims извлекаются `user_id`, `is_superuser`, `company_ids`,
`service_accounts`, `permissions` и т.д.
2. **Zitadel** — если передан дополнительный заголовок
`Identity: Bearer <jwt>`, полезная нагрузка берётся из этого токена
(`urn:zitadel:iam:user:metadata`); основной токен кладётся как
`access_token`. Подпись identity-токена приложением не проверяется
(`ParseUnverified`) — доверие обеспечивается сетевым слоем.
При отсутствии заголовка `Authorization`, неверной схеме (не `Bearer`) или
ошибке разбора токена возвращается **401 Unauthorized** (пустое тело).
Пути `/internal/*` аутентификации на уровне приложения не требуют.
### Пагинация
Списочные эндпоинты используют пагинацию через query-параметры `limit` и
`offset` (напр. `internal/controller/http/v1/document/downloaded.go`,
`flat.go`). Единого конверта ответа нет — формат зависит от эндпоинта.
### Обработка ошибок
Ошибки бизнес-слоя оборачиваются в `AppError`
(`internal/app_errors/errors.go`) и сериализуются как JSON
`{ "message": "...", "error_code": "GW-XXXX" }`. HTTP-статус выбирается по
`error_code` в `internal/app_errors/middleware.go`:
| error_code | Статус | Значение |
| --- | --- | --- |
| `GW-0001` | 404 | Ресурс не найден |
| `GW-0002` | 401 | Не аутентифицирован |
| `GW-0003` | 403 | Нет доступа |
| `GW-0004` | 400 | Некорректный запрос |
| `GW-0014` | 400 | Ошибка валидации состояния |
| `GW-0000` | 500 | Системная ошибка |
Не все роутеры используют обёртку `middleware.ErrorHandler` — часть
обработчиков возвращает ошибки/статусы напрямую, поэтому формат ответа об
ошибке может отличаться от `AppError`.
### Замечания (расхождения кода)
- Пути с сегментом `*` (напр. `/api/v1/disks/*/documents`,
`/api/v1/documents/*/attributes`, `/api/v1/targets/*/remarks/*`) — это
«жадные» wildcard-сегменты Fiber, захватывающие путь документа/цели
целиком (включая `/`). В спецификации они представлены параметром пути.
- Часть эндпоинтов завершается слэшем (`/`), часть — нет; поведение
определяется определениями групп во Fiber.
- v0-роутер (`internal/controller/http/v0`, на `labstack/echo`) в рантайме
не подключён.
contact:
name: pdm
url: https://gitlab.com/sarex-team/pdm
servers:
- url: http://pdm-api.documentations:8080
description: Stage (внутренний адрес в кластере, namespace documentations)
- url: http://pdm-api.documentations-preprod:8080
description: Preprod (внутренний адрес в кластере)
- url: http://pdm-api.documentations-prod:8080
description: Production (внутренний адрес в кластере)
security:
- bearerAuth: []
tags:
- name: documents
description: Документы, диски, проекты, атрибуты (v1/v2/v3/v4)
- name: resources
description: Ресурсы и разрешения (v1/v2)
- name: remarks
description: Замечания по таргетам
- name: attachments
description: Вложения
- name: states
description: Состояния рабочих областей
- name: subscriptions
description: Подписки
- name: system_log
description: Системный лог
- name: targets
description: Таргеты
- name: inspections
description: Инспекции
- name: users
description: Пользователи
- name: eav
description: Атрибуты (EAV)
- name: releases
description: Релизы
- name: notes
description: Заметки
- name: drawings
description: Чертежи (сечения и экспорты)
- name: internal
description: Служебные эндпоинты (без аутентификации)
paths:
# ---------------- v1: documents ----------------
/api/v1/disks/{diskPath}/documents:
get:
tags: [documents]
summary: Список документов по диску (v1)
parameters:
- $ref: '#/components/parameters/DiskPath'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/disks/{diskPath}/downloaded_documents:
get:
tags: [documents]
summary: Список скачанных документов диска
parameters:
- $ref: '#/components/parameters/DiskPath'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/disks/{diskPath}/flat_documents:
get:
tags: [documents]
summary: Плоский список документов диска
parameters:
- $ref: '#/components/parameters/DiskPath'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/disks/{diskId}/thumbnails:
post:
tags: [documents]
summary: Превью документов диска
parameters:
- name: diskId
in: path
required: true
schema: { type: string }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/projects/:
get:
tags: [documents]
summary: Получить проект по ID документа
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents:
post:
tags: [documents]
summary: Создать документ
responses:
'200': { $ref: '#/components/responses/Ok' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/{documentPath}/attributes:
get:
tags: [documents]
summary: Атрибуты документа
parameters:
- $ref: '#/components/parameters/DocumentPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
tags: [documents]
summary: Создать/обновить атрибуты документа
parameters:
- $ref: '#/components/parameters/DocumentPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/{documentPath}/subscription/:
delete:
tags: [documents]
summary: Удалить подписку по ID документа
parameters:
- $ref: '#/components/parameters/DocumentPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/bin:
get:
tags: [documents]
summary: Список удалённых документов (корзина)
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/approving_users:
post:
tags: [documents]
summary: Согласующие пользователи
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/bundle_versions:
post:
tags: [documents]
summary: Версии бандлов документов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/ancestors:
post:
tags: [documents]
summary: Предки документов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/size:
get:
tags: [documents]
summary: Размер папки
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/related_documents:
get:
tags: [documents]
summary: Связанные документы
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [documents]
summary: Создать связи документов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/documents/related_documents/bulk_delete:
post:
tags: [documents]
summary: Массовое удаление связей документов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: remarks ----------------
/api/v1/targets/{targetPath}/remarks:
post:
tags: [remarks]
summary: Создать замечание для таргета
parameters:
- $ref: '#/components/parameters/TargetPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/targets/{targetPath}/remarks/{remarkPath}:
get:
tags: [remarks]
summary: Получить замечание
parameters:
- $ref: '#/components/parameters/TargetPath'
- $ref: '#/components/parameters/RemarkPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [remarks]
summary: Обновить замечание
parameters:
- $ref: '#/components/parameters/TargetPath'
- $ref: '#/components/parameters/RemarkPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: resources ----------------
/api/v1/resources/:
get:
tags: [resources]
summary: Список ресурсов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resources/users-with-resources/:
post:
tags: [resources]
summary: Пользователи с ресурсами
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resources/permissions-bulk/:
patch:
tags: [resources]
summary: Массовое обновление разрешений
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resources/{uuid}/:
get:
tags: [resources]
summary: Ресурс по UUID
parameters:
- name: uuid
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resource-permissions/:
get:
tags: [resources]
summary: Разрешения на ресурсы
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [resources]
summary: Массовое создание разрешений на ресурс
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resource-permissions/{id}/:
delete:
tags: [resources]
summary: Удалить разрешение на ресурс
parameters:
- $ref: '#/components/parameters/IdPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/bulk_delete/resource-permissions/:
post:
tags: [resources]
summary: Массовое удаление разрешений на ресурс
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/company-resource-permissions/:
get:
tags: [resources]
summary: Разрешения компаний на ресурсы
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [resources]
summary: Создать разрешение компании на ресурс
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/company-resource-permissions/{id}/:
delete:
tags: [resources]
summary: Удалить разрешение компании на ресурс
parameters:
- $ref: '#/components/parameters/IdPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resources-rpc/resource-by-document-id/{id}/:
get:
tags: [resources]
summary: Ресурс по ID документа
parameters:
- $ref: '#/components/parameters/IdPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/resources-rpc/parent-document-by-resource-id/{uuid}/:
get:
tags: [resources]
summary: Родительская папка проекта по UUID ресурса
parameters:
- name: uuid
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: attachments ----------------
/api/v1/attachments:
post:
tags: [attachments]
summary: Создать вложение
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [attachments]
summary: Список вложений
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/attachments/{id}:
get:
tags: [attachments]
summary: Вложение по ID
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
tags: [attachments]
summary: Удалить вложение
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: states (workspace) ----------------
/api/v1/workspace/{ws_id}:
post:
tags: [states]
summary: Создать состояние с вложением
parameters:
- $ref: '#/components/parameters/WsId'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [states]
summary: Состояния с вложениями
parameters:
- $ref: '#/components/parameters/WsId'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/workspace/{ws_id}/dynamic_states:
post:
tags: [states]
summary: Создать динамические состояния
parameters:
- $ref: '#/components/parameters/WsId'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [states]
summary: Динамические состояния
parameters:
- $ref: '#/components/parameters/WsId'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/workspace/states/{id}:
get:
tags: [states]
summary: Состояние по ID
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/workspace/dynamic_states/{id}:
get:
tags: [states]
summary: Динамическое состояние по ID
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: subscriptions ----------------
/api/v1/subscription/:
post:
tags: [subscriptions]
summary: Создать подписку
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: system_log ----------------
/api/v1/system_log/:
get:
tags: [system_log]
summary: Отфильтрованный системный лог
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [system_log]
summary: Записать в системный лог
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: targets ----------------
/api/v1/targets/:
get:
tags: [targets]
summary: Список таргетов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/targets/{targetPath}/:
get:
tags: [targets]
summary: Таргет по ID
parameters:
- $ref: '#/components/parameters/TargetPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: inspections ----------------
/api/v1/inspections/:
post:
tags: [inspections]
summary: Создать инспекцию
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [inspections]
summary: Список инспекций
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/{id}:
get:
tags: [inspections]
summary: Инспекция по ID
parameters:
- $ref: '#/components/parameters/IdInt'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [inspections]
summary: Обновить инспекцию
parameters:
- $ref: '#/components/parameters/IdInt'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
tags: [inspections]
summary: Удалить инспекцию
parameters:
- $ref: '#/components/parameters/IdInt'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/created_at/daterange:
get:
tags: [inspections]
summary: Диапазон дат создания
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/inspection_date/daterange:
get:
tags: [inspections]
summary: Диапазон дат инспекций
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/available_responsible_users:
get:
tags: [inspections]
summary: Доступные ответственные пользователи
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/available_inspection_dates:
get:
tags: [inspections]
summary: Доступные даты инспекций
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/inspections/export:
get:
tags: [inspections]
summary: Экспорт инспекций
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: users / eav / releases / notes ----------------
/api/v1/users/:
get:
tags: [users]
summary: Пользователи по разрешению на ресурс
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/attribute/:
get:
tags: [eav]
summary: Получить атрибуты (EAV)
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/releases/{id}:
get:
tags: [releases]
summary: Список релизов по ID проекта
parameters:
- $ref: '#/components/parameters/IdInt'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/notes/{service_id}/{entity_id}/{instance_id}/:
get:
tags: [notes]
summary: Заметки по сервису/сущности/инстансу
parameters:
- { name: service_id, in: path, required: true, schema: { type: string } }
- { name: entity_id, in: path, required: true, schema: { type: string } }
- { name: instance_id, in: path, required: true, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v1: drawings ----------------
/api/v1/drawings/cross-sections:
post:
tags: [drawings]
summary: Создать сечение
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [drawings]
summary: Сечения по ID инстанса
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/drawings/cross-sections/{cross_section_id}/data:
get:
tags: [drawings]
summary: Данные сечения
parameters:
- { name: cross_section_id, in: path, required: true, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/drawings/cross-sections/{cross_section_id}:
delete:
tags: [drawings]
summary: Удалить сечение
parameters:
- { name: cross_section_id, in: path, required: true, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/drawings/exports:
post:
tags: [drawings]
summary: Создать экспорт
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
get:
tags: [drawings]
summary: Список экспортов
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v1/drawings/exports/{export_id}:
delete:
tags: [drawings]
summary: Удалить экспорт
parameters:
- { name: export_id, in: path, required: true, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v2 ----------------
/api/v2/resources/:
get:
tags: [resources]
summary: Расширенный список ресурсов (v2)
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v2/resources:
post:
tags: [resources]
summary: Создать расширенный ресурс (v2)
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v2/resources/{id}:
get:
tags: [resources]
summary: Расширенный ресурс по ID (v2)
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [resources]
summary: Обновить расширенный ресурс (v2)
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
tags: [resources]
summary: Удалить расширенный ресурс (v2)
parameters:
- $ref: '#/components/parameters/IdPathPlain'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v2/users/:
get:
tags: [users]
summary: Пользователи по ресурсу (v2)
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v2/disks/{diskPath}/documents:
get:
tags: [documents]
summary: Список документов по диску (v2)
parameters:
- $ref: '#/components/parameters/DiskPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- v3 / v4 ----------------
/api/v3/disks/{diskPath}/documents:
get:
tags: [documents]
summary: Упрощённый список документов по диску (v3)
parameters:
- $ref: '#/components/parameters/DiskPath'
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/v4/disks/{disk_id}/documents:
get:
tags: [documents]
summary: Поиск документов по диску (v4)
parameters:
- { name: disk_id, in: path, required: true, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/Ok' }
'401': { $ref: '#/components/responses/Unauthorized' }
# ---------------- internal ----------------
/internal/v1/documents/approving_users:
post:
tags: [internal]
summary: Согласующие пользователи (внутренний, без аутентификации)
security: []
responses:
'200': { $ref: '#/components/responses/Ok' }
/internal/healthz/startup:
get:
tags: [internal]
summary: Startup-проба (БД, опционально Valkey)
security: []
responses:
'200': { description: OK }
'503': { description: Service Unavailable }
/internal/healthz/live:
get:
tags: [internal]
summary: Liveness-проба
security: []
responses:
'200': { description: OK }
/internal/healthz/ready:
get:
tags: [internal]
summary: Readiness-проба
security: []
responses:
'200': { description: OK }
'503': { description: Service Unavailable }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT в заголовке `Authorization: Bearer <jwt>`. В режиме Zitadel
дополнительно передаётся заголовок `Identity: Bearer <jwt>`.
parameters:
Limit:
name: limit
in: query
required: false
schema: { type: integer, format: int64, minimum: 0 }
description: Размер страницы
Offset:
name: offset
in: query
required: false
schema: { type: integer, format: int64, minimum: 0 }
description: Смещение
DiskPath:
name: diskPath
in: path
required: true
schema: { type: string }
description: Жадный wildcard-сегмент Fiber (может содержать `/`) — путь/ID диска
DocumentPath:
name: documentPath
in: path
required: true
schema: { type: string }
description: Жадный wildcard-сегмент Fiber — путь/ID документа
TargetPath:
name: targetPath
in: path
required: true
schema: { type: string }
description: Жадный wildcard-сегмент Fiber — путь/ID таргета
RemarkPath:
name: remarkPath
in: path
required: true
schema: { type: string }
description: Жадный wildcard-сегмент Fiber — путь/ID замечания
WsId:
name: ws_id
in: path
required: true
schema: { type: string }
description: UUID рабочей области
IdPath:
name: id
in: path
required: true
schema: { type: string }
IdPathPlain:
name: id
in: path
required: true
schema: { type: string }
IdInt:
name: id
in: path
required: true
schema: { type: integer }
description: Числовой идентификатор (Fiber-ограничение `:id<int>`)
responses:
Ok:
description: Успешный ответ (структура зависит от эндпоинта)
content:
application/json:
schema: {}
BadRequest:
description: Некорректный запрос
content:
application/json:
schema: { $ref: '#/components/schemas/AppError' }
Unauthorized:
description: Не аутентифицирован (пустое тело)
Forbidden:
description: Нет доступа
content:
application/json:
schema: { $ref: '#/components/schemas/AppError' }
NotFound:
description: Ресурс не найден
content:
application/json:
schema: { $ref: '#/components/schemas/AppError' }
schemas:
AppError:
type: object
description: Формат ошибки бизнес-слоя (`internal/app_errors/errors.go`)
properties:
message:
type: string
example: not found
error_code:
type: string
description: Внутренний код ошибки (`GW-XXXX`)
enum: [GW-0000, GW-0001, GW-0002, GW-0003, GW-0004, GW-0014]
example: GW-0001
required: [message, error_code]

View File

@ -0,0 +1,30 @@
# drawings-api — пример переменных окружения.
# Конфигурация читается через github.com/kelseyhightower/envconfig
# (config/config.go). Приложение НЕ загружает .env автоматически —
# переменные нужно экспортировать в окружение процесса самому,
# напр.: set -a && . ./.env && set +a
# API
API_ADDRESS=localhost:6666
# Postgres
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=drawings
POSTGRES_ADDRESS=localhost:6432
POSTGRES_POOL_SIZE=10
# TLS-подключение к БД. При ENABLE_SSL=true используется сертификат
# из YC-PG-CERTIFICATE (PEM-содержимое, не путь к файлу)
ENABLE_SSL=false
YC-PG-CERTIFICATE=
# Workflow (интеграция с workflows-api через sdk-go)
WORKFLOW_HOST=http://workflows-api-service.proc/
CONTAINER_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q
IMAGE_NAME_EXPORT_TO_DWG=cross-sections-to-dwg
IMAGE_TAG=develop
TASK_VERSION=1
# Внутренний URL самого drawings-api — на него workflow вызывает webhook
DRAWING_INTERNAL_URL=http://drawings-api-service.aero/
# URL сервиса attachments (передаётся в задачу экспорта)
ATTACHMENT_URL=http://attachments-service.documentations.svc.cluster.local:80

View File

@ -0,0 +1,120 @@
# Конфигурация проекта drawings-api
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис написан на **Go** (`gitlab.com/sarex-team/rnd/drawings-api`) и настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`kelseyhightower/envconfig`](https://github.com/kelseyhightower/envconfig) (`envconfig.Process("", &Config)`).
Особенности разбора:
- **префикса нет** — переменные читаются по именам из тега `envconfig:"..."` (напр. `POSTGRES_ADDRESS`, `API_ADDRESS`);
- вложенных секций через разделитель нет: `Config` — плоская композиция трёх структур (`Postgres`, `API`, `Workflow`), у каждого поля своё явное имя переменной;
- часть полей имеет дефолт через тег `default:"..."` (напр. `CONTAINER_REGISTRY`, `TASK_VERSION`); поля без дефолта при отсутствии переменной получают нулевое значение типа (пустая строка / `0` / `false`), ошибки старта из-за «обязательности» нет;
- при ошибке разбора (`envconfig.Process`) приложение завершается с `logger.Fatalf` (`config.MustParse`).
Отдельного конфиг-файла (yaml/toml) у приложения нет. Файл `.env` в репозитории — только шаблон; приложение его **не загружает автоматически** (в коде нет чтения `.env`/dotenv), переменные нужно экспортировать в окружение самому.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса. `.env` — шаблон, экспортируется вручную, напр. `set -a && . ./.env && set +a`. Сборка — `make drawings-api` / `make migrations` |
| Локально (контейнер) | `Dockerfile` (multi-stage, `golang:1.22`) + `entrypoint.sh`. Переменные пробрасываются через `--env`/`--env-file` при запуске контейнера |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`) и выбор окружения по ветке/тегу через `workflow.rules` |
Порядок запуска в контейнере (`entrypoint.sh`): сначала применяются миграции (`migrations migrate`), затем стартует основной бинарник (`drawings-api`).
Точки входа (`cmd/`):
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `drawings-api` | `cmd/drawings-api` | HTTP API-сервер (gorilla/mux) |
| `migrations migrate` | `cmd/migrations` | Применение миграций БД (`robinjoseph08/go-pg-migrations`) |
## Переменные приложения
Все переменные ниже читаются кодом приложения (`config/config.go`). В столбце «Значение по умолчанию» указан дефолт из тега `default:"..."`; `—` означает, что дефолта нет (при отсутствии переменной поле получает нулевое значение типа).
### API (`API`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера, напр. `0.0.0.0:8080` |
### Postgres (`Postgres`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_USER` | string | — | Пользователь БД |
| `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД |
| `POSTGRES_DB` | string | — | Имя базы данных |
| `POSTGRES_ADDRESS` | string | — | Адрес PostgreSQL в формате `host:port` |
| `POSTGRES_POOL_SIZE` | int | — | Размер пула соединений (`pg.Options.PoolSize`) |
| `ENABLE_SSL` | bool | — | Подключение к БД по TLS. При `true` строится `tls.Config` из `YC-PG-CERTIFICATE` |
| `YC-PG-CERTIFICATE` | string | — | PEM-содержимое CA-сертификата PostgreSQL (не путь к файлу). Используется только при `ENABLE_SSL=true` |
> При `ENABLE_SSL=true` из содержимого `YC-PG-CERTIFICATE` собирается пул корневых сертификатов; `ServerName` берётся из хостовой части `POSTGRES_ADDRESS`, при этом в коде выставлен `InsecureSkipVerify: true`. Имя переменной `YC-PG-CERTIFICATE` содержит дефисы (нестандартно для env), но именно так указано в теге `envconfig`.
### Workflow (`Workflow`)
Интеграция с сервисом workflows через `gitlab.com/sarex-team/sdk-go/pkg/workflows`: создание workflow экспорта cross-section в DWG (задача парсинга + задача webhook-уведомления).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOW_HOST` | string | — | Базовый URL сервиса workflows |
| `CONTAINER_REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов для задач workflow |
| `IMAGE_NAME_EXPORT_TO_DWG` | string | — | Имя образа задачи экспорта cross-section в DWG |
| `IMAGE_TAG` | string | — | Тег образов задач workflow |
| `TASK_VERSION` | string | `1` | Версия задачи экспорта (параметр `version`) |
| `DRAWING_INTERNAL_URL` | string | — | Внутренний URL самого drawings-api; на него workflow вызывает webhook `POST {DRAWING_INTERNAL_URL}internal/v1/exports/{export_id}/webhook` |
| `ATTACHMENT_URL` | string | — | URL сервиса attachments (передаётся в задачу экспорта как `attachment_url`) |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Деплой выполняется через `universal-chart` (`Chart.yaml`, зависимость `universal-chart`). Сервис `drawings-api` слушает порт `8080`; probes настроены на `/ping` (в чарте выключены). Обычные значения задаются в блоке `envs`, значения из секретов — в `secretEnvs`. Значения различаются по окружениям через ключи `_default` / `stage` / `preprod` / `production`.
Обычные значения (`envs`) — те же переменные приложения, что описаны выше (`API_ADDRESS`, `ENABLE_SSL`, `WORKFLOW_HOST`, `CONTAINER_REGISTRY`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `TASK_VERSION`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL`), различаются адресами сервисов, тегами образов и флагом `ENABLE_SSL` по окружениям.
Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`_default`) | Секрет (`stage`) | Ключ |
| --- | --- | --- | --- |
| `POSTGRES_USER` | `ya-pg-secret` | `drawings-postgresql-secret` | `username` |
| `POSTGRES_PASSWORD` | `ya-pg-secret` | `drawings-postgresql-secret` | `password` |
| `POSTGRES_DB` | `ya-pg-secret` | `drawings-postgresql-secret` | `database` |
| `POSTGRES_POOL_SIZE` | `ya-pg-secret` | `drawings-postgresql-secret` | `pool-size` |
| `POSTGRES_ADDRESS` | `ya-pg-secret` | `drawings-postgresql-secret` | `address` |
| `YC-PG-CERTIFICATE` | `yc-pg-certificate` | `drawings-postgresql-secret` | `ca.crt` |
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml` ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH |
| --- | --- | --- | --- | --- |
| ветка `stage` | `stage` | `aero` | `0.0.1-stage` | `universal-chart-stage` |
| ветка `master` | `preprod` | `drawings-preprod` | `0.0.1-preprod` | `universal-chart-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `drawings-prod` | `0.0.1-prod` | `universal-chart-production` |
| merge request | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Ключевые переменные пайплайна: `SERVICE_NAME=drawings-api`, `DOCKERFILE_PATH=./Dockerfile`, `RELEASE_NAME=drawings-api`, `CHART_NAME=${SERVICE_NAME}`, `BUILD_ARGS` (`--build-arg CI_COMMIT_SHORT_SHA=…`), `HELM_SET_ARGS` (`--set universal-chart.services.drawings-api.image.name.<env>=…`, `--set universal-chart.global.env=<env>`, а также `commitSha`/`gitlabUri`/`gitlabJobUrl`/`owner`).
## Замечания и потенциальные проблемы
- **Нет обязательности полей.** В отличие от pydantic-конфигов других сервисов, `envconfig` не помечает поля обязательными — при отсутствии переменной поле молча получает нулевое значение. Например, пустой `POSTGRES_ADDRESS` не вызовет ошибку старта конфига, но приведёт к ошибке при подключении к БД.
- **Имя `YC-PG-CERTIFICATE` с дефисами** нестандартно для переменных окружения, но именно так задано в теге `envconfig` и в Helm-секрете. В отличие от других сервисов, здесь это **содержимое** сертификата (PEM), а не путь к файлу.
- **TLS к БД с `InsecureSkipVerify: true`.** При `ENABLE_SSL=true` корневой сертификат подхватывается, но проверка имени/цепочки фактически ослаблена флагом `InsecureSkipVerify`.
- **Webhook-петля.** `DRAWING_INTERNAL_URL` должен указывать на сам drawings-api внутри кластера — по нему workflow дергает `POST internal/v1/exports/{export_id}/webhook` для перевода экспорта в статус `done`. Неверный URL оставит экспорты в статусе `running`.
- **`.env` не загружается автоматически** — переменные нужно экспортировать вручную либо задавать через окружение контейнера.
## Минимальный набор для локального запуска
Минимально необходимо задать:
- `API_ADDRESS` (напр. `localhost:6666`)
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_ADDRESS`, `POSTGRES_POOL_SIZE`, `ENABLE_SSL` (`false` локально; тогда `YC-PG-CERTIFICATE` не нужен)
- `WORKFLOW_HOST`, `IMAGE_NAME_EXPORT_TO_DWG`, `IMAGE_TAG`, `DRAWING_INTERNAL_URL`, `ATTACHMENT_URL` (для сценариев экспорта; `CONTAINER_REGISTRY` и `TASK_VERSION` имеют дефолты)
Готовые значения-примеры приведены в `.env.example`.

426
apps/drawings/openapi.yaml Normal file
View File

@ -0,0 +1,426 @@
openapi: 3.0.3
info:
title: Drawings API
version: "0.0.1"
description: |
REST API сервиса **drawings-api** (`gitlab.com/sarex-team/rnd/drawings-api`) —
управление разрезами (cross-sections) чертежей, их данными и экспортом
в DWG через сервис workflows.
Сервис написан на **Go** (gorilla/mux, go-pg). Роутер собирается в
`cmd/drawings-api/bootstrap.go`. Помимо служебных эндпоинтов
(`/ping`, `/metrics`) есть две группы бизнес-маршрутов с одинаковым
набором операций:
- `/api/v1/*` — публичный роутинг;
- `/internal/v1/*` — внутренний роутинг (набор тот же плюс webhook
экспорта, вызываемый воркером workflow).
На все бизнес-маршруты навешены middleware: JSON-ответ (`rest.JSONResponse`),
request-id (`reqid.Middleware`) и логирование. Явной аутентификации в коде
сервиса нет — доступ ограничивается на уровне ingress/сети кластера.
### Экспорт в DWG
`POST /exports` создаёт запись экспорта и запускает workflow из двух задач
(парсинг cross-section в DWG + webhook-уведомление). По завершении workflow
вызывает `POST /internal/v1/exports/{export_id}/webhook`, который переводит
экспорт в статус `done`.
servers:
- url: /api/v1
description: Публичный префикс
- url: /internal/v1
description: Внутренний префикс
tags:
- name: service
description: Служебные эндпоинты
- name: cross-sections
description: Разрезы чертежей
- name: exports
description: Экспорт разрезов в DWG
paths:
/ping:
get:
tags: [service]
summary: Healthcheck
description: Возвращает статус готовности. Доступен в корне (без префикса).
responses:
"200":
description: Сервис готов
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ready
/metrics:
get:
tags: [service]
summary: Prometheus-метрики
description: Метрики в формате Prometheus. Доступен в корне (без префикса).
responses:
"200":
description: Метрики
content:
text/plain:
schema:
type: string
# ----- Публичные маршруты (/api/v1) и внутренние (/internal/v1) идентичны,
# кроме webhook, который есть только на /internal/v1. Пути ниже указаны
# относительно префикса из блока servers. -----
/cross-sections:
post:
tags: [cross-sections]
summary: Создать разрез
description: Создаёт cross-section вместе с его данными (`data.raw_data`).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateCrossSectionRequest"
responses:
"200":
description: Созданный разрез
content:
application/json:
schema:
$ref: "#/components/schemas/CrossSection"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
get:
tags: [cross-sections]
summary: Список разрезов по instance_id
parameters:
- name: instance_id
in: query
required: true
description: UUID инстанса (чертежа)
schema:
type: string
format: uuid
responses:
"200":
description: Массив разрезов (пустой, если ничего не найдено)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/CrossSection"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
/cross-sections/{cs_id}:
delete:
tags: [cross-sections]
summary: Удалить разрез (soft-delete)
parameters:
- $ref: "#/components/parameters/CrossSectionId"
responses:
"200":
$ref: "#/components/responses/OK"
"400":
$ref: "#/components/responses/BadRequest"
"404":
description: Разрез не найден (возможно, уже удалён)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/StorageError"
/cross-sections/{cs_id}/data:
get:
tags: [cross-sections]
summary: Данные разреза
parameters:
- $ref: "#/components/parameters/CrossSectionId"
responses:
"200":
description: Данные разреза
content:
application/json:
schema:
$ref: "#/components/schemas/Data"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
/exports:
post:
tags: [exports]
summary: Создать экспорт разреза в DWG
description: |
Создаёт запись экспорта и запускает workflow экспорта в DWG.
В ответе `workflow_status` = `running`.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateExportRequest"
responses:
"200":
description: Созданный экспорт
content:
application/json:
schema:
$ref: "#/components/schemas/Export"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
get:
tags: [exports]
summary: Список экспортов по фильтрам
description: |
Хотя бы один из фильтров должен быть задан, иначе `400`.
Каждый параметр — список UUID/чисел через запятую.
parameters:
- name: cross_section_ids
in: query
required: false
description: UUID разрезов через запятую
schema:
type: string
- name: export_ids
in: query
required: false
description: UUID экспортов через запятую
schema:
type: string
- name: workflow_ids
in: query
required: false
description: UUID workflow через запятую
schema:
type: string
- name: attachment_ids
in: query
required: false
description: ID вложений (целые) через запятую
schema:
type: string
responses:
"200":
description: Массив экспортов (пустой, если ничего не найдено)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Export"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
/exports/{export_id}:
delete:
tags: [exports]
summary: Удалить экспорт
parameters:
- $ref: "#/components/parameters/ExportId"
responses:
"200":
$ref: "#/components/responses/OK"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
/exports/{export_id}/webhook:
post:
tags: [exports]
summary: Webhook завершения экспорта (только /internal/v1)
description: |
Вызывается воркером workflow по завершении экспорта. Переводит
экспорт в статус `done`. Доступен только по внутреннему префиксу
`/internal/v1`.
parameters:
- $ref: "#/components/parameters/ExportId"
responses:
"200":
$ref: "#/components/responses/OK"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/StorageError"
components:
parameters:
CrossSectionId:
name: cs_id
in: path
required: true
description: UUID разреза
schema:
type: string
format: uuid
ExportId:
name: export_id
in: path
required: true
description: UUID экспорта
schema:
type: string
format: uuid
responses:
OK:
description: Успешно (тело — строка `"OK"`)
content:
application/json:
schema:
type: string
example: OK
BadRequest:
description: Некорректный запрос
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
StorageError:
description: Внутренняя ошибка (ошибка хранилища)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
schemas:
CrossSection:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
created_at:
type: string
format: date-time
deleted_at:
type: string
format: date-time
instance_id:
type: string
format: uuid
documents:
type: object
additionalProperties:
$ref: "#/components/schemas/ConnectedDocument"
exports:
type: array
items:
$ref: "#/components/schemas/Export"
ConnectedDocument:
type: object
properties:
color:
type: string
name:
type: string
Data:
type: object
properties:
cross_section_id:
type: string
format: uuid
raw_data:
type: string
CreateCrossSectionRequest:
type: object
description: Разрез плюс его данные. Наследует поля CrossSection.
allOf:
- $ref: "#/components/schemas/CrossSection"
- type: object
properties:
data:
$ref: "#/components/schemas/Data"
Author:
type: object
properties:
id:
type: integer
format: int64
first_name:
type: string
last_name:
type: string
Export:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
author:
$ref: "#/components/schemas/Author"
created_at:
type: string
format: date-time
deleted_at:
type: string
format: date-time
nullable: true
cross_section_id:
type: string
format: uuid
workflow_id:
type: string
format: uuid
nullable: true
workflow_status:
type: string
nullable: true
enum: [done, running, error]
file_type:
type: string
enum: [dwg]
attachment_id:
type: integer
nullable: true
CreateExportRequest:
type: object
required: [cross_section_id, author, file_type, company_id]
properties:
cross_section_id:
type: string
format: uuid
author:
$ref: "#/components/schemas/Author"
file_type:
type: string
enum: [dwg]
company_id:
type: integer
format: int64
Error:
type: object
description: Ответ об ошибке (gotools/httperror).
properties:
error:
type: string

54
apps/eav/.env.example Normal file
View File

@ -0,0 +1,54 @@
# Django
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_DEBUG=False
DJANGO_SECRET_KEY='v628rpgi^!!57jq9y7y3^by04c1bc@#6%0_a(ekxfmyat8gxew'
# App
SERVICE_NAME=eav
VERSION=1.0.0
# Database (PostgreSQL) — читается только в config.settings.production
DJANGO_POSTGRES_HOST=127.0.0.1
DJANGO_POSTGRES_PORT=6432
DJANGO_POSTGRES_DATABASE=eav_db
DJANGO_POSTGRES_USER=sarex
DJANGO_POSTGRES_PASSWORD=password
# Auth / JWT (RS512) — обязательны в config.settings.production
SIMPLE_JWT_ISSUER=django
# Replace newlines with \n
JWT_PRIVATE_KEY=''
JWT_PUBLIC_KEY=''
# S3 (Yandex Object Storage, бото3)
YC_S3_ACCESS_KEY_ID=
YC_S3_SECRET_ACCESS_KEY=
YC_S3_BUCKET_NAME=eav
YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net
# Kafka
KAFKA_ENABLED=True
KAFKA_HOST=
KAFKA_USERNAME=platform
KAFKA_PASSWORD=
KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt
SASL_MECHANISM=SCRAM-SHA-512
SECURITY_PROTOCOL=SASL_SSL
ASSETS_TOPIC=assets_broadcast_test
# Kafka topics (события EAV)
KAFKA_TOPIC_ATTRIBUTE_CREATED=eav.attribute.created.v1
KAFKA_TOPIC_ATTRIBUTE_UPDATED=eav.attribute.updated.v1
KAFKA_TOPIC_ATTRIBUTE_DELETED=eav.attribute.deleted.v1
KAFKA_TOPIC_VALUE_OPTION_CREATED=eav.value_option.created.v1
KAFKA_TOPIC_VALUE_OPTION_DELETED=eav.value_option.deleted.v1
# OpenTelemetry (трейсинг включается только если USE_OTEL задана)
USE_OTEL=False
SERVICE_NAME=eav.eav-backend
TRACER_ENDPOINT=localhost:4375
USE_INSECURE=False
ENVIRONMENT=prod
MODULE=eav
TEAM=platform_team
COMPONENT=backend

186
apps/eav/CONFIGURATION.md Normal file
View File

@ -0,0 +1,186 @@
# Конфигурация проекта eav-python
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис — это Django-приложение (**Django 4.1 + Django REST Framework**), запускаемое как WSGI (`config.wsgi`) через **uWSGI** (порт `8000`, см. `compose/eav-backend/uwsgi.ini`). Настройки читаются из переменных окружения в `src/config/settings/base.py` и `src/config/settings/production.py`. Разбор выполняется частично через библиотеку [`django-environ`](https://django-environ.readthedocs.io/) (объект `env = environ.Env()`), частично напрямую через `os.getenv`.
Особенности разбора:
- **префикса/делимитера у секций нет** — каждая настройка задаётся плоской переменной окружения (напр. `DJANGO_POSTGRES_HOST`, `KAFKA_HOST`, `YC_S3_BUCKET_NAME`);
- **`.env` не загружается автоматически** — в коде нет вызова `environ.Env.read_env()` / `load_dotenv`, хотя `python-dotenv` присутствует в зависимостях. Переменные нужно экспортировать в окружение самому (напр. `set -a && . ./.env && set +a`) либо задавать через `--env`/манифесты k8s. Файл `.env` при этом в `.gitignore`;
- **выбор набора настроек** задаётся `DJANGO_SETTINGS_MODULE`: `config.settings.production` (боевой набор с БД, CORS, JWT), `config.settings.test` (только `base`), либо `config.settings.local` (по умолчанию в `manage.py`, в репозитории отсутствует, `.gitignore`);
- **часть переменных читается только в `production.py`** — БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`); в `base.py`/`test.py` их нет.
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (manage.py / uWSGI) | Переменные окружения процесса (`.env` нужно экспортировать вручную) |
| Локально (docker-compose) | `docker-compose.yml`: блок `environment` сервиса `backend` + образ `postgres` (timescaledb-postgis) |
| Kubernetes (Helm, репозиторий приложения) | `.helm/values-<env>.yaml`: блоки `backend.deployment.envs` (обычные значения) и `backend.deployment.secrets` (из k8s-секретов через `secretKeyRef`); шаблон `templates/server.yaml`, роутинг — `templates/mesh-config.yaml` (Istio VirtualService) |
| Kubernetes (infra, Flux/Kustomize) | `infra/iac/apps/eav/base/backend-deployment.yaml`: секреты инъектируются Vault-агентом (`vault.hashicorp.com/agent-inject-*`) и экспортируются в окружение в `args`; настройки `production.py` монтируются из `django-configmap` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: общие шаблоны `generic/common-ci`, переменные пайплайна в `workflow.rules` |
Способы запуска процессов:
| Процесс | Точка входа | Назначение |
| --- | --- | --- |
| HTTP API | `compose/eav-backend/entrypoint.sh``uwsgi --ini uwsgi.ini` (`config.wsgi`, порт 8000) | REST API |
| Миграции | `entrypoint.sh``python3 manage.py migrate` (выполняется перед стартом uWSGI) | Миграции БД |
| Kafka-продюсер | `config/kafka.py` (инициализируется при импорте, если `KAFKA_ENABLED`) | Публикация событий EAV в топики |
Порядок запуска в контейнере (`entrypoint.sh`): сначала `manage.py migrate`, затем `uwsgi` (оба под `opentelemetry-instrument`).
## Переменные приложения
Дефолт `—` означает, что значения по умолчанию в коде нет.
### Django / приложение
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_SETTINGS_MODULE` | string | `config.settings.local``manage.py`); в контейнере — `config.settings.production` | Какой набор настроек Django загружать |
| `DJANGO_DEBUG` | bool | `False` | Режим отладки Django (в `production.py` жёстко `False`) |
| `DJANGO_SECRET_KEY` | string | (захардкоженный дефолт) | Секретный ключ Django. В проде обязателен свой |
| `SERVICE_NAME` | string | `eav` | Имя сервиса (в `base.py`); в OTEL-секции дефолт `eav.eav-backend` |
| `VERSION` | string | `1.0.0` | Версия приложения |
### Database — PostgreSQL (только `config.settings.production`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_POSTGRES_HOST` | string | — | Хост PostgreSQL |
| `DJANGO_POSTGRES_PORT` | int | `6432` | Порт PostgreSQL (в infra-манифесте — `5432`) |
| `DJANGO_POSTGRES_DATABASE` | string | — | Имя базы данных |
| `DJANGO_POSTGRES_USER` | string | — | Пользователь БД |
| `DJANGO_POSTGRES_PASSWORD` | string | — | Пароль пользователя БД |
> Engine — `django.db.backends.postgresql`. В `docker-compose.yml` поднимается `timescale/timescaledb-postgis` (проекту нужны расширения PostGIS/ltree).
### Auth / JWT (только `config.settings.production`)
Используются два механизма аутентификации (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): `ZitadelJWTAuthentication` (заголовок `Identity`) и `rest_framework_simplejwt` (RS512).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SIMPLE_JWT_ISSUER` | string | `django` | Значение claim `iss` (проверяется при верификации токена) |
| `JWT_PRIVATE_KEY` | string (PEM) | — | Приватный RSA-ключ (подпись). Экранированные `\n` заменяются на переводы строк. Обязателен |
| `JWT_PUBLIC_KEY` | string (PEM) | — | Публичный RSA-ключ (проверка). Экранированные `\n` заменяются на переводы строк. Обязателен |
> `SIMPLE_JWT`: алгоритм `RS512`, `ACCESS_TOKEN_LIFETIME` 5 мин, `REFRESH_TOKEN_LIFETIME` 1 день, тип заголовка `Bearer`, claim пользователя — `user_id`.
### S3 — Yandex Object Storage (`base.py`, boto3/django-storages)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `YC_S3_ACCESS_KEY_ID` | string | `None` | Access key |
| `YC_S3_SECRET_ACCESS_KEY` | string | `None` | Secret key |
| `YC_S3_BUCKET_NAME` | string | `None` | Бакет по умолчанию |
| `YC_S3_ENDPOINT_URL` | string | `None` | Эндпоинт S3 |
> `DEFAULT_FILE_STORAGE`/`STATICFILES_STORAGE` — `storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL=public-read`.
### Kafka (`base.py`, `config/kafka.py`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_ENABLED` | bool | `True` | Включить реального продюсера (иначе `MockProducer` — события не отправляются) |
| `KAFKA_HOST` | string | `""` | Адрес брокера (`bootstrap_servers`) |
| `KAFKA_USERNAME` | string | `platform` | Пользователь SASL |
| `KAFKA_PASSWORD` | string | `""` | Пароль SASL |
| `KAFKA_SSL_CAFILE` | string | `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt` | CA-сертификат для TLS |
| `SASL_MECHANISM` | string | `SCRAM-SHA-512` | Механизм SASL |
| `SECURITY_PROTOCOL` | string | `SASL_SSL` | Протокол безопасности Kafka |
| `ASSETS_TOPIC` | string | `assets_broadcast_test` | Топик рассылки по ассетам |
Топики событий EAV:
| Переменная | Значение по умолчанию |
| --- | --- |
| `KAFKA_TOPIC_ATTRIBUTE_CREATED` | `eav.attribute.created.v1` |
| `KAFKA_TOPIC_ATTRIBUTE_UPDATED` | `eav.attribute.updated.v1` |
| `KAFKA_TOPIC_ATTRIBUTE_DELETED` | `eav.attribute.deleted.v1` |
| `KAFKA_TOPIC_VALUE_OPTION_CREATED` | `eav.value_option.created.v1` |
| `KAFKA_TOPIC_VALUE_OPTION_DELETED` | `eav.value_option.deleted.v1` |
### OpenTelemetry (`base.py`)
Блок трейсинга активируется, только если задана переменная `USE_OTEL` (проверяется через `os.getenv('USE_OTEL', False)` — истинно при любом непустом значении). Используется `django-otel-tools`; при включении в начало `MIDDLEWARE` добавляется `OtelMiddleware`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_OTEL` | bool/string | `False` | Включить трейсинг и OTEL-логгер |
| `SERVICE_NAME` | string | `eav.eav-backend` | Имя сервиса в трейсах |
| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора |
| `USE_INSECURE` | bool/string | `False` | Небезопасное (без TLS) подключение к коллектору |
| `ENVIRONMENT` | string | `prod` | Атрибут ресурса `environment` |
| `MODULE` | string | `eav` | Атрибут ресурса `module` |
| `TEAM` | string | `platform_team` | Атрибут ресурса `team` |
| `COMPONENT` | string | `backend` | Атрибут ресурса `component` |
## Переменные из Helm-чарта (`.helm/values-<env>.yaml`)
Обычные значения задаются в блоке `backend.deployment.envs` для каждого окружения (`stage`/`preprod`/`production`): `DJANGO_SETTINGS_MODULE`, `USE_OTEL`, `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE`, `ENVIRONMENT`, `KAFKA_HOST`, `ASSETS_TOPIC` (различаются адресами коллектора/брокера и именами топиков).
Значения из секретов (блок `secrets`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secret_name`) | Ключ (`secret_key`) |
| --- | --- | --- |
| `DJANGO_POSTGRES_HOST` | `yc-pg-secret` | `host` |
| `DJANGO_POSTGRES_DATABASE` | `yc-pg-secret` | `database` |
| `DJANGO_POSTGRES_PORT` | `yc-pg-secret` | `port` (только preprod) |
| `DJANGO_POSTGRES_USER` | `yc-pg-secret` | `user` |
| `DJANGO_POSTGRES_PASSWORD` | `yc-pg-secret` | `password` |
| `DJANGO_CLICKHOUSE_HOST` | `yc-ch-secret` | `host` |
| `DJANGO_CLICKHOUSE_DATABASE` | `yc-ch-secret` | `database` |
| `DJANGO_CLICKHOUSE_USER` | `yc-ch-secret` | `user` |
| `DJANGO_CLICKHOUSE_PASSWORD` | `yc-ch-secret` | `password` |
| `YC_S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` |
| `YC_S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` |
| `YC_S3_BUCKET_NAME` | `yc-s3-secret` | `storage_bucket_name` |
| `YC_S3_ENDPOINT_URL` | `yc-s3-secret` | `endpoint_url` |
| `JWT_PRIVATE_KEY` | `jwt-secret` | `private_key` |
| `JWT_PUBLIC_KEY` | `jwt-secret` | `public_key` |
| `KAFKA_USERNAME` | `kafka-secret` / `yc-kafka-secret` | `username` |
| `KAFKA_PASSWORD` | `kafka-secret` / `yc-kafka-secret` | `password` |
| `KAFKA_HOST` | `yc-kafka-secret` | `host` (prod/stage) |
Помимо env, чарт монтирует CA-сертификаты: PostgreSQL (`yc-pg-certificate` → `~/.postgresql/root.crt`) и Yandex Internal Root CA (`yc-ch-certificate` → `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt`, тот же путь, что в `KAFKA_SSL_CAFILE`), а также конфиг clickhouse-client.
## Переменные в infra-манифесте (Flux/Kustomize, `infra/iac/apps/eav`)
В отличие от Helm-чарта приложения, боевой деплой Sarex использует Vault-инъекцию (`base/backend-deployment.yaml`). Секреты рендерятся Vault-агентом в файлы `/vault/secrets/*` и экспортируются в окружение в `args` контейнера перед запуском `entrypoint.sh`:
| Переменная(ые) | Источник (Vault path) |
| --- | --- |
| `DJANGO_POSTGRES_HOST/PORT/DATABASE/USER/PASSWORD` | `secrets/data/postgresql/apps/eav` |
| `YC_S3_ENDPOINT_URL/BUCKET_NAME/ACCESS_KEY_ID/SECRET_ACCESS_KEY` | `secrets/data/minio/apps/eav` |
| `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` | `secrets/data/vault/common/rsa_keys` |
Прямо в `env` деплоймента задаются `KAFKA_ENABLED=False`, `ASSETS_TOPIC=sarex`, `DJANGO_SETTINGS_MODULE=config.settings.production`. Файл `production.py` монтируется из `django-configmap` (переопределяет `production.py` из образа; в нём `DEBUG=True`, `ALLOWED_HOSTS=['*']`, свои CORS/CSRF-домены и имена cookie `eav-sessionid`/`eav-csrftoken`).
## Замечания и потенциальные проблемы
- Приложение **не загружает `.env` автоматически** (нет `read_env`/`load_dotenv`). `python-dotenv` установлен, но не используется в настройках — переменные нужно экспортировать в окружение самому.
- Переменные БД (`DJANGO_POSTGRES_*`) и JWT (`JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY`) читаются **только** в `config.settings.production`. При `test`/`base` их отсутствие не мешает старту, но БД по умолчанию не сконфигурирована.
- `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY` в `production.py` читаются через `env.str(...)` **без дефолта** — их отсутствие приводит к ошибке старта. В infra-варианте (`django-configmap`) используется `get_env_variable` с тем же требованием.
- `DJANGO_CLICKHOUSE_*` присутствуют в Helm-секретах, но **кодом приложения не читаются** (в текущих настройках ClickHouse не используется) — это подготовка/наследие инфраструктуры.
- `KAFKA_ENABLED`: при ложном значении используется `MockProducer` — события EAV в Kafka не публикуются (так сделано в infra-деплое: `KAFKA_ENABLED=False`). Значение разбирается `django-environ` как bool.
- Флаги OTEL (`USE_OTEL`, `USE_INSECURE`) читаются через `os.getenv(..., False)` и трактуются как истинные при **любой непустой строке**, включая `"False"`. Чтобы отключить — переменную нужно не задавать вовсе.
- `SERVICE_NAME` определяется дважды: как имя приложения (`base.py`, дефолт `eav`) и как имя сервиса в OTEL (дефолт `eav.eav-backend`) — фактически одна и та же переменная окружения.
- В `docker-compose.yml` захардкожен пароль БД (`zealot096`) — только для локального окружения.
## Минимальный набор для локального запуска (`config.settings.production`)
- `DJANGO_SETTINGS_MODULE=config.settings.production`
- `DJANGO_POSTGRES_HOST`, `DJANGO_POSTGRES_PORT`, `DJANGO_POSTGRES_DATABASE`, `DJANGO_POSTGRES_USER`, `DJANGO_POSTGRES_PASSWORD`
- `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` (обязательны; можно тестовую RSA-пару)
- `KAFKA_ENABLED=False` (чтобы не поднимать брокер) либо `KAFKA_HOST`/`KAFKA_USERNAME`/`KAFKA_PASSWORD`
- при работе с файлами: `YC_S3_ACCESS_KEY_ID`, `YC_S3_SECRET_ACCESS_KEY`, `YC_S3_BUCKET_NAME`, `YC_S3_ENDPOINT_URL`
- `USE_OTEL` — не задавать (иначе включится трейсинг)
Готовые значения-примеры для всех переменных приведены в `.env.example`.

1303
apps/eav/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

187
apps/flows/.env.example Normal file
View File

@ -0,0 +1,187 @@
# =============================================================================
# flows-backend (.env.example)
# =============================================================================
# Приложение читает переменные окружения напрямую через pydantic BaseSettings
# (src/flow/config.py). У верхнеуровневого класса Settings префикса нет — его
# поля задаются переменными с именем поля в ВЕРХНЕМ регистре (напр. BASE_HOST).
# Вложенные секции конфигурируются отдельными классами со своим env_prefix
# (PG_, DJANGO_, DOCUMENTATION_, RABBITMQ_, TRACING_ и т.д.).
#
# Приложение НЕ загружает .env автоматически (нет python-dotenv/env_file) —
# экспортируйте переменные в окружение самостоятельно, напр.:
# set -a && . ./.env && set +a
# =============================================================================
# App / общие настройки (класс Settings, без префикса)
SERVICE_NAME=review-service
SERVICE_HOST=0.0.0.0
SERVICE_PORT=8000
# Префикс за реверс-прокси (в кластере: /flows)
PROXY_PATH_PREFIX=
API_PREFIX=/api/v1
API_INTERNAL_PREFIX=/internal/v1
BASE_HOST=https://lk.sarex.io
DEBUG=False
# Проверка подписи JWT (True в кластере; False удобно для локальной разработки)
JWT_AUTH_ENABLE=True
# Таймаут gunicorn (используется в entrypoint.sh, не кодом)
TIMEOUT=120
# Feature-флаги
ENABLE_MAILINGS=True
ENABLE_MAILGUN=True
ENABLE_CELERY=True
ENABLE_EVENTS=True
ENABLE_ANALYTICS=False
SYNC_RESOURCE_ID=False
# Logger (класс LoggerSettings, префикс LOG_)
LOG_LEVEL=INFO
# Database — основной PostgreSQL (класс PostgresSettings, префикс PG_)
PG_HOST=127.0.0.1
PG_PORT=6432
PG_LOGIN=flow
PG_PASSWORD=password
PG_DB=flows_db
# Documentation PG — БД сервиса документаций (класс DocumentationDBSettings, префикс DOCUMENTATION_PG_)
DOCUMENTATION_PG_HOST=127.0.0.1
DOCUMENTATION_PG_PORT=6432
DOCUMENTATION_PG_USERNAME=flow
DOCUMENTATION_PG_PASSWORD=password
DOCUMENTATION_PG_DATABASE=flows_db
# RabbitMQ (класс RabbitSettings, префикс RABBITMQ_)
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=flow
RABBITMQ_PASSWORD=flow
RABBITMQ_VHOST=flows
# Celery (класс CelerySettings, префикс CELERY_; брокер берётся из RABBITMQ_*)
CELERY_QUEUE=flow
# Sarex backend (Django) (класс DjangoSettings, префикс DJANGO_)
DJANGO_USE=True
DJANGO_HOST=http://localhost:8000/api
DJANGO_TIMEOUT=60
# base64(login:password) для Basic-auth
DJANGO_TOKEN=
# Documentation service (класс DocumentationSettings, префикс DOCUMENTATION_)
DOCUMENTATION_USE=True
DOCUMENTATION_HOST=https://api.sarex.io/documentations/api/v1
DOCUMENTATION_EXTERNAL_HOST=https://api.sarex.io/documentations/api/v1
DOCUMENTATION_TIMEOUT=60
# EAV service (класс EAVSettings, префикс EAV_)
EAV_HOST=http://eav-service.eav-prod
EAV_TIMEOUT=60
# Planning management / MSP (класс PlanningManagementSettings, префикс PLANNING_)
PLANNING_USE=True
PLANNING_HOST=https://api.sarex.io/api/pm/msp
PLANNING_TIMEOUT=60
# Checklists service (класс CheckListsSettings, префикс CHECKLIST_)
CHECKLIST_USE=True
CHECKLIST_HOST=https://stage-api.sarex.io/checklists
CHECKLIST_TIMEOUT=60
# Workflows service (класс WorkflowsSettings, префикс WORKFLOWS_)
WORKFLOWS_USE=True
WORKFLOWS_HOST=https://lk.sarex.io/workflows/api/v1
WORKFLOWS_TIMEOUT=60
# Gateway / Resources (поля класса Settings, используются при SYNC_RESOURCE_ID=1)
GATEWAY_URL=https://stage-api.sarex.io/gateway
RESOURCE_URL=https://stage-api.sarex.io/resources
# Event bus (класс EventBusSettings, префикс EVENTS_)
EVENTS_HOST=ws://localhost:8000/ws
EVENTS_CONNECTION_TIMEOUT=5
EVENTS_COUNT_RETRIES=100
# Admin panel (класс AdminPanelSettings, префикс ADMIN_PANEL_)
ADMIN_PANEL_SECRET_KEY=hex
ADMIN_PANEL_TOKEN_MAX_AGE=86400
# Auth (RSA public key для проверки JWT; в кластере монтируется как JWT_PUBLIC_KEY)
JWT_PUBLIC_KEY=
# Sentry (класс SentrySettings, префикс SENTRY_)
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_SEND_DEFAULT_PII=True
# Почта: SMTP (поля класса Settings; альтернатива Mailgun)
SMTP_HOST=
SMTP_PORT=
FROM_EMAIL=
# OpenTelemetry / трейсинг (класс TraceSettings, префикс TRACING_)
TRACING_USE=False
TRACING_HOST=localhost:4317
TRACING_INSECURE=False
TRACING_SERVICE_NAME=flows
TRACING_ENVIRONMENT=prod
TRACING_MODULE=flows
TRACING_TEAM=team_proc
TRACING_COMPONENT=backend
# Прочее (не читается приложением, задаётся в инфраструктуре)
# ENABLE_METRICS=0
# =============================================================================
# Переменные ТОЛЬКО для celery-воркера и scheduler
# (src/worker/notifications_config.py, src/worker/sync_config.py)
# =============================================================================
# Flows DB (класс FlowsDatabase, префикс FLOWS_DB_)
FLOWS_DB_HOST=127.0.0.1
FLOWS_DB_PORT=6432
FLOWS_DB_DB=flows_db
FLOWS_DB_USERNAME=flow
FLOWS_DB_PASSWORD=password
# Issues DB (класс IssuesDatabase, префикс ISSUES_DB_)
ISSUES_DB_HOST=127.0.0.1
ISSUES_DB_PORT=6432
ISSUES_DB_DB=issues_db
ISSUES_DB_USERNAME=issues
ISSUES_DB_PASSWORD=password
# RFI DB (класс RFIDatabase, префикс RFI_DB_)
RFI_DB_HOST=127.0.0.1
RFI_DB_PORT=6432
RFI_DB_DB=rfi_db
RFI_DB_USERNAME=rfi
RFI_DB_PASSWORD=password
# Django-клиент воркера (класс DjangoClient, префикс DJANGO_)
DJANGO_BASE_HOST=https://lk.sarex.io
# DJANGO_HOST — см. выше
# DJANGO_AUTH — base64(login:password); в кластере берётся из секрета django
DJANGO_AUTH=
# Resources-клиент воркера (класс ResourcesClient, префикс RESOURCES_)
RESOURCES_HOST=http://iams.iam.svc.cluster.local:8080
# Flows-клиент воркера (класс FlowsClient, префикс FLOWS_)
FLOWS_HOST=https://api.sarex.io/flows
# Настройки рассылок воркера (класс NotificationGlobalSettings, префикс NOTIFICATION_SETTINGS_)
NOTIFICATION_SETTINGS_ENABLE_MAILINGS=True
NOTIFICATION_SETTINGS_USE_MAILGUN=True
# Mailgun (класс MailgunClient, префикс MAILGUN_)
MAILGUN_HOST=https://api.mailgun.net/v3/mg.sarex.io
MAILGUN_API_KEY=
# Отправка уведомлений через Workflows (класс WorkflowsNotificationsSettings, префикс WORKFLOWS_NOTIFICATIONS_)
WORKFLOWS_NOTIFICATIONS_REGISTRY=cr.yandex/crp3ccidau046kdj8g9q
WORKFLOWS_NOTIFICATIONS_SMTP_HOST=127.0.0.1
WORKFLOWS_NOTIFICATIONS_SMTP_PORT=42069
WORKFLOWS_NOTIFICATIONS_FROM_EMAIL=hello@sarex.io

293
apps/flows/CONFIGURATION.md Normal file
View File

@ -0,0 +1,293 @@
# Конфигурация проекта flows-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/flow/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `Settings` и набор вложенных классов `*Settings`).
Особенности разбора:
- у верхнеуровневого класса `Settings` **префикса нет** и не задан `env_nested_delimiter`его собственные поля задаются переменными с именем поля в верхнем регистре (напр. `BASE_HOST`, `SERVICE_PORT`, `PROXY_PATH_PREFIX`);
- каждая вложенная секция — это **отдельный класс** `BaseSettings` со своим `env_prefix` (`class Config: env_prefix = "..."`), который читает переменные окружения независимо. Поэтому переменные «плоские» с префиксами: `PG_HOST`, `DJANGO_HOST`, `RABBITMQ_PORT`, `TRACING_USE` и т.д. — двойного подчёркивания для вложенности здесь нет;
- отсутствие обязательного поля без дефолта приводит к ошибке старта; большинство полей приложения имеют дефолты (см. таблицы ниже).
Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически**`config.py` не задан `env_file`, зависимости `python-dotenv` нет) — переменные нужно экспортировать в окружение самому, напр. `set -a && . ./.env && set +a`.
Воркер (`src/worker`) использует **собственные** классы настроек (`src/worker/notifications_config.py`, `src/worker/sync_config.py`, `src/worker/celery.py`) с частично другими префиксами (`FLOWS_DB_`, `ISSUES_DB_`, `RFI_DB_`, `RESOURCES_`, `MAILGUN_`, `WORKFLOWS_NOTIFICATIONS_` и т.д.).
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально | Переменные окружения процесса. `.env.example` — шаблон; приложение его **не** загружает автоматически, экспортируйте вручную |
| Контейнер | `Dockerfile` / `Dockerfile.worker`; запуск через `entrypoint.sh` (сначала `alembic upgrade head`, затем gunicorn) |
| Kubernetes (Helm, репозиторий) | `.helm/values.yaml` (`universal-chart`): блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `backend`, `worker`, `scheduler` |
| Kubernetes (kustomize, infra) | `iac/apps/flows/base/*.yaml`: env в `backend-deployment.yaml` / `celery-deployment.yaml`; секреты инжектируются агентом **HashiCorp Vault** (`vault.hashicorp.com/agent-inject-*`) и подгружаются в окружение перед стартом |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`), общий шаблон `generic/common-ci` (`universal-pipeline.yaml`) |
Способы запуска процессов:
| Процесс | Команда | Назначение |
| --- | --- | --- |
| HTTP API | `gunicorn ... flow.main:app` (`entrypoint.sh`) | Публичный и внутренний REST API (FastAPI) |
| Celery worker | `celery -A src.worker worker` | Обработчик фоновых задач (рассылки, синхронизация) |
| Celery beat (scheduler) | `celery -A src.worker beat -l INFO` | Периодические задачи (см. `beat_schedule` ниже) |
| Alembic | `alembic upgrade head``entrypoint.sh`) | Миграции БД при старте контейнера |
Порядок запуска в контейнере (`entrypoint.sh`): миграции (`alembic upgrade head`), затем `gunicorn -w 3 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --timeout $TIMEOUT ... flow.main:app`.
Периодические задачи воркера (`src/worker/celery.py`, `beat_schedule`):
| Задача | Расписание (UTC) | Назначение |
| --- | --- | --- |
| `sync_reviews` | `*/7` минут | Синхронизация review |
| `notify_users` | пн–пт, 06:00 | Рассылка уведомлений пользователям |
| `notify_admins_about_empty_steps` | пн–пт, 05:30 | Уведомление админов о пустых шагах |
## Переменные приложения (API)
Дефолт `—` означает, что значение обязательно (иначе ошибка старта).
### Общие (`Settings`, без префикса)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SERVICE_NAME` | string | `review-service` | Имя сервиса |
| `SERVICE_HOST` | string | `0.0.0.0` | Адрес прослушивания (в кластере переопределяется внешним URL API) |
| `SERVICE_PORT` | int | `8000` | Порт |
| `PROXY_PATH_PREFIX` | string | `""` | Root path за реверс-прокси (в кластере `/flows`). Влияет на `root_path` FastAPI и на префикс админки |
| `API_PREFIX` | string | `/api/v1` | Префикс публичного API |
| `API_INTERNAL_PREFIX` | string | `/internal/v1` | Префикс внутреннего API |
| `BASE_HOST` | string | `https://lk.sarex.io` | Базовый внешний URL (для ссылок/писем) |
| `GATEWAY_URL` | string | `https://stage-api.sarex.io/gateway` | URL gateway (используется при `SYNC_RESOURCE_ID=1`) |
| `RESOURCE_URL` | string | `https://stage-api.sarex.io/resources` | URL сервиса ресурсов/IAM (используется при `SYNC_RESOURCE_ID=1`) |
| `REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q` | Реестр образов |
| `JWT_AUTH_ENABLE` | bool | `True` | Включить аутентификацию по JWT. `False` — все запросы идут от дефолтного пользователя (удобно локально) |
| `DEBUG` | bool | `False` | Режим отладки |
| `ENABLE_MAILINGS` | bool | `True` | Включить рассылки |
| `ENABLE_MAILGUN` | bool | `True` | Использовать Mailgun (иначе — SMTP) |
| `ENABLE_CELERY` | bool | `True` | Включить постановку задач в Celery |
| `ENABLE_EVENTS` | bool | `True` | Включить событийную шину |
| `ENABLE_ANALYTICS` | bool | `False` | Отправлять данные в аналитику |
| `SYNC_RESOURCE_ID` | bool | `False` | Определять `resource_id` через gateway/resources при создании review/документов |
| `SMTP_HOST` | string \| null | `None` | SMTP-хост (альтернатива Mailgun) |
| `SMTP_PORT` | int \| null | `None` | SMTP-порт |
| `FROM_EMAIL` | string \| null | `None` | Адрес отправителя писем |
### Logger (`LoggerSettings`, префикс `LOG_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `INFO` | Уровень логирования (`INFO`/`DEBUG`/…); JSON-формат вывода |
| `LOG_FORMAT` | string | JSON-шаблон | Формат строки лога |
### Auth
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `JWT_PUBLIC_KEY` | string | — | Публичный RSA-ключ для проверки подписи JWT. В кластере монтируется из секрета и экспортируется как `JWT_PUBLIC_KEY` перед стартом (см. `entrypoint`/Vault) |
> Аутентификация выполняется в `src/flow/middleware.py` (`TokenUserMiddleware`). При наличии заголовка `identity` полезная нагрузка берётся из Zitadel-токена (`urn:zitadel:iam:user:metadata`), иначе — из основного `Authorization: Bearer <jwt>`. Подпись проверяется публичным ключом. Пути `/docs/`, `/openapi.json/`, `/internal/` из проверки исключены.
### Database — основной PostgreSQL (`PostgresSettings`, префикс `PG_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PG_HOST` | string | `""` | Хост PostgreSQL |
| `PG_PORT` | string | `6432` | Порт PostgreSQL (обычно pgbouncer) |
| `PG_LOGIN` | string | `""` | Пользователь БД |
| `PG_PASSWORD` | string | `""` | Пароль БД |
| `PG_DB` | string | `""` | Имя базы данных |
> Итоговый DSN собирается свойством `PostgresSettings.url`: `postgresql://{login}:{password}@{host}:{port}/{db}`.
### Documentation PG (`DocumentationDBSettings`, префикс `DOCUMENTATION_PG_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DOCUMENTATION_PG_HOST` | string | `""` | Хост БД документаций |
| `DOCUMENTATION_PG_PORT` | string | `""` | Порт |
| `DOCUMENTATION_PG_USERNAME` | string | `""` | Пользователь |
| `DOCUMENTATION_PG_PASSWORD` | string | `""` | Пароль |
| `DOCUMENTATION_PG_DATABASE` | string | `""` | Имя базы |
### RabbitMQ (`RabbitSettings`, префикс `RABBITMQ_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `RABBITMQ_HOST` | string | `localhost` | Хост |
| `RABBITMQ_PORT` | string | `5672` | Порт |
| `RABBITMQ_USERNAME` | string | `flow` | Пользователь |
| `RABBITMQ_PASSWORD` | string | `flow` | Пароль |
| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост (в кластере `flows`/`flow_preprod`/`flow_prod`) |
### Celery (`CelerySettings`, префикс `CELERY_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CELERY_QUEUE` | string | `flow` | Очередь задач. Брокер — из `RABBITMQ_*` |
### HTTP-клиенты внешних сервисов
Каждый клиент — отдельный класс с полями `use`/`host`/`timeout` (и своим префиксом). Соединение создаётся httpx-клиентом.
| Секция / префикс | Переменные | Назначение |
| --- | --- | --- |
| Sarex backend (Django) — `DJANGO_` | `DJANGO_USE` (`True`), `DJANGO_HOST` (`http://localhost:8000/api`), `DJANGO_TIMEOUT` (`60`), `DJANGO_TOKEN` (base64 `login:password` для Basic-auth) | Основной backend Sarex |
| Documentations — `DOCUMENTATION_` | `DOCUMENTATION_USE` (`True`), `DOCUMENTATION_HOST`, `DOCUMENTATION_EXTERNAL_HOST`, `DOCUMENTATION_TIMEOUT` (`60`) | Сервис документаций (внутренний и внешний хост) |
| EAV — `EAV_` | `EAV_HOST` (`http://eav-service.eav-prod`), `EAV_TIMEOUT` (`60`) | Сервис EAV (атрибуты) |
| Planning / MSP — `PLANNING_` | `PLANNING_USE` (`True`), `PLANNING_HOST` (`https://api.sarex.io/api/pm/msp`), `PLANNING_TIMEOUT` (`60`) | Планирование |
| Checklists — `CHECKLIST_` | `CHECKLIST_USE` (`True`), `CHECKLIST_HOST`, `CHECKLIST_TIMEOUT` (`60`) | Сервис чек-листов |
| Workflows — `WORKFLOWS_` | `WORKFLOWS_USE` (`True`), `WORKFLOWS_HOST` (`https://lk.sarex.io/workflows/api/v1`), `WORKFLOWS_TIMEOUT` (`60`) | Сервис workflows |
### Event bus (`EventBusSettings`, префикс `EVENTS_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `EVENTS_HOST` | string | `ws://localhost:8000/ws` | Адрес WebSocket событийной шины |
| `EVENTS_CONNECTION_TIMEOUT` | int | `5` | Таймаут подключения (сек) |
| `EVENTS_COUNT_RETRIES` | int | `100` | Число попыток переподключения |
### Admin panel (`AdminPanelSettings`, префикс `ADMIN_PANEL_`)
Админка (`sqladmin`) монтируется по пути `/api/admin/` (с учётом `PROXY_PATH_PREFIX`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ADMIN_PANEL_SECRET_KEY` | string | `hex` | Секретный ключ сессии админки |
| `ADMIN_PANEL_TOKEN_MAX_AGE` | int | `86400` | Время жизни токена (сек) |
### Sentry (`SentrySettings`, префикс `SENTRY_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SENTRY_DSN` | string | `""` | DSN Sentry |
| `SENTRY_ENVIRONMENT` | string | `production` | Окружение |
| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов |
| `SENTRY_SEND_DEFAULT_PII` | bool | `True` | Отправлять PII |
### OpenTelemetry / трейсинг (`TraceSettings`, префикс `TRACING_`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRACING_USE` | bool | `False` | Включить трейсинг (при `True` инициализируется OTLP + middleware) |
| `TRACING_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора |
| `TRACING_INSECURE` | bool | `False` | Подключение без TLS |
| `TRACING_SERVICE_NAME` | string | `flows` | Имя сервиса в трейсах |
| `TRACING_ENVIRONMENT` | string | `prod` | Окружение (`stage`/`preprod`/`prod`) |
| `TRACING_MODULE` | string | `flows` | Атрибут `module` |
| `TRACING_TEAM` | string | `team_proc` | Атрибут `team` |
| `TRACING_COMPONENT` | string | `backend` | Атрибут `component` |
## Переменные только для воркера и scheduler
Читаются классами из `src/worker/*`, а не основным приложением.
### Базы данных воркера
| Секция / префикс | Переменные | Назначение |
| --- | --- | --- |
| Flows DB — `FLOWS_DB_` | `FLOWS_DB_HOST`, `FLOWS_DB_PORT`, `FLOWS_DB_DB`, `FLOWS_DB_USERNAME`, `FLOWS_DB_PASSWORD` | БД flows (для задач синхронизации/рассылок) |
| Issues DB — `ISSUES_DB_` | `ISSUES_DB_HOST`, `ISSUES_DB_PORT`, `ISSUES_DB_DB`, `ISSUES_DB_USERNAME`, `ISSUES_DB_PASSWORD` | БД issues |
| RFI DB — `RFI_DB_` | `RFI_DB_HOST`, `RFI_DB_PORT`, `RFI_DB_DB`, `RFI_DB_USERNAME`, `RFI_DB_PASSWORD` | БД RFI |
Все пять полей каждой БД обязательны (без дефолтов).
### Клиенты и рассылки воркера
| Секция / префикс | Переменные | Назначение |
| --- | --- | --- |
| Django-клиент — `DJANGO_` | `DJANGO_BASE_HOST`, `DJANGO_HOST`, `DJANGO_AUTH` (Basic-auth) | Получение пользователей/токенов |
| Resources-клиент — `RESOURCES_` | `RESOURCES_HOST` | Пользователи, сгруппированные по ресурсам |
| Flows-клиент — `FLOWS_` | `FLOWS_HOST` | Внутренние вызовы flows API (`switch_to_next_step`) |
| Глобальные настройки рассылок — `NOTIFICATION_SETTINGS_` | `NOTIFICATION_SETTINGS_ENABLE_MAILINGS` (`True`), `NOTIFICATION_SETTINGS_USE_MAILGUN` (`True`) | Флаги рассылок |
| Mailgun — `MAILGUN_` | `MAILGUN_HOST`, `MAILGUN_API_KEY`, `MAILGUN_SENT_FROM` (`hello@sarex.io`) | Отправка писем через Mailgun |
| Workflows — `WORKFLOWS_` | `WORKFLOWS_HOST`, `WORKFLOWS_TIMEOUT` (`60`) | Постановка job в workflows |
| Уведомления через Workflows — `WORKFLOWS_NOTIFICATIONS_` | `WORKFLOWS_NOTIFICATIONS_TAG` (`email`), `WORKFLOWS_NOTIFICATIONS_REGISTRY`, `WORKFLOWS_NOTIFICATIONS_SMTP_HOST`, `WORKFLOWS_NOTIFICATIONS_SMTP_PORT`, `WORKFLOWS_NOTIFICATIONS_FROM_EMAIL` | Параметры job-рассылки |
## Переменные инфраструктуры, сборки и деплоя
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `TIMEOUT` | `entrypoint.sh` | Таймаут gunicorn-воркеров (сек), напр. `120` (stage) / `900` (prod) |
| `ENABLE_METRICS` | `.helm/values.yaml` | Флаг метрик (`0`/`1`), кодом не читается |
| `PIP_INDEX_URL` / `--extra-index-url` | `requirements.txt` | Приватный индекс пакетов Nexus (`fastapi-otel-tools`) |
| `SERVICE_HOST` (в кластере) | `.helm/values.yaml`, kustomize | В кластере в `SERVICE_HOST` кладётся внешний URL API (`https://api.sarex.io/flows/api/v1`), переопределяя дефолт `0.0.0.0` |
| `SAREX_MAILER_HOST` | `.helm/values.yaml` | Хост mailer-сервиса |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт `universal-chart` описывает три сервиса — `backend`, `worker`, `scheduler`. Обычные значения задаются в блоке `envs` (с ключами по окружениям `_default`/`stage`/`preprod`/`production`), значения из секретов — в блоке `secretEnvs` (монтируются как env через `secretKeyRef`).
Значения из секретов (backend):
| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `PG_DB` | `postgres-secret` / `flows-postgresql-secret` | `database` |
| `PG_LOGIN` | `postgres-secret` / `flows-postgresql-secret` | `username` |
| `PG_PASSWORD` | `postgres-secret` / `flows-postgresql-secret` | `password` |
| `PG_HOST` | `postgres-secret` / `flows-postgresql-secret` | `host` |
| `SENTRY_DSN` | `sentry-secret` | `dsn` |
| `SENTRY_ENVIRONMENT` | `sentry-secret` | `env` |
| `DJANGO_TOKEN` | `django-secret` | `token` |
| `RABBITMQ_USERNAME` | `rabbitmq-secret` / `flows-rabbitmq-secret` | `username` |
| `RABBITMQ_PASSWORD` | `rabbitmq-secret` / `flows-rabbitmq-secret` | `password` |
| `ADMIN_PANEL_SECRET_KEY` | `admin-secret` | `key` |
| `JWT_PUBLIC_KEY` | `jwt-secret` | `public_key` |
| `DOCUMENTATION_PG_*` | `documentations-postgresql-secret` / `documentations-postgres-secret` | `database`/`host`/`port`/`username`/`password` |
Воркер дополнительно получает секреты `FLOWS_DB_*`, `ISSUES_DB_*`, `RFI_DB_*` (из соответствующих postgres-секретов), `DJANGO_AUTH` (`django-secret.token`), `MAILGUN_API_KEY` (`mailgun-secret.api-key`).
Чарт также монтирует CA-сертификат PostgreSQL (`pg-cert` → `/root/.postgresql/root.crt`).
## Переменные из kustomize-манифестов (`iac/apps/flows`)
Инфраструктурный репозиторий разворачивает те же образы через kustomize (`base` + оверлеи `brusnika-stage`, `brusnika-prod`, `yc-k8s-test`). Секреты инжектируются агентом **HashiCorp Vault** (аннотации `vault.hashicorp.com/agent-inject-*`) и подгружаются в окружение из файлов `/vault/secrets/*` перед запуском `entrypoint.sh`:
| Секрет Vault | Переменные |
| --- | --- |
| `secrets/data/postgresql/apps/flows` | `PG_DB`, `PG_LOGIN`, `PG_HOST`, `PG_PORT`, `PG_PASSWORD`, `DOCUMENTATION_PG_*` |
| `secrets/data/rabbitmq/apps/flows` | `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_VHOST`, `RABBITMQ_HOST`, `RABBITMQ_PORT` |
| `secrets/data/vault/common/django_auth` | `DJANGO_TOKEN` |
| `secrets/data/vault/common/rsa_keys` | `JWT_PUBLIC_KEY` (public_key) |
Остальные значения (`LOG_LEVEL`, `BASE_HOST`, `DJANGO_HOST`, `DOCUMENTATION_HOST`, `EAV_HOST`, `GATEWAY_URL`, `RESOURCE_URL`, `SERVICE_HOST`, `WORKFLOWS_HOST`, `CHECKLIST_HOST`, `SMTP_HOST`/`SMTP_PORT`, `FROM_EMAIL`, `ENABLE_*`, `SYNC_RESOURCE_ID`, `TIMEOUT` и т.д.) задаются напрямую в блоке `env` deployment-манифеста оверлея.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `proc` | `0.0.1-stage` |
| ветка `master` | `preprod` | `flows-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `flows-prod` | `0.0.1-prod` |
Ключевые переменные пайплайна: `SERVICE_NAME=flows-backend`, `DOCKERFILE_PATH=Dockerfile`, `IMAGE_NAME_WORKER` (образ воркера, собирается job-ом `build_worker` из `Dockerfile.worker`), `HELM_SET_ARGS` (проброс образов backend/worker/scheduler и метаданных коммита в чарт).
## Замечания и потенциальные проблемы
- Приложение **не загружает `.env` автоматически** — переменные нужно экспортировать в окружение (см. `.env.example`).
- У `Settings` нет `env_nested_delimiter`, поэтому вложенные секции конфигурируются **плоскими** переменными со своими префиксами (`PG_`, `DJANGO_`, `RABBITMQ_`, …), а не через `__`.
- Поле `service_host` (дефолт `0.0.0.0`) и переменная `SERVICE_HOST` совпадают по имени: в кластере в `SERVICE_HOST` кладётся внешний URL API, что переопределяет адрес прослушивания в объекте настроек. Реальный адрес/порт прослушивания при запуске в контейнере задаёт gunicorn (`-b 0.0.0.0:8000` в `entrypoint.sh`), а не поле настроек.
- Почта: при `ENABLE_MAILGUN=1` используется Mailgun (`MAILGUN_*` — в основном на стороне воркера), иначе — SMTP (`SMTP_HOST`/`SMTP_PORT`/`FROM_EMAIL`).
- Healthcheck-эндпоинта у сервиса нет; в чарте probes (`liveness`/`readiness`) отключены.
- Воркер использует отдельные классы настроек (pydantic v1 стиль `class Config`), у которых поля БД **обязательны** — при запуске воркера без `FLOWS_DB_*`/`ISSUES_DB_*`/`RFI_DB_*` будет ошибка.
## Минимальный набор для локального запуска
Минимально необходимо задать:
- `SERVICE_PORT` (по умолчанию `8000`), `PROXY_PATH_PREFIX` (пусто локально)
- `JWT_AUTH_ENABLE=False` (чтобы не требовался `JWT_PUBLIC_KEY`) — иначе задайте `JWT_PUBLIC_KEY`
- `PG_HOST`, `PG_PORT`, `PG_LOGIN`, `PG_PASSWORD`, `PG_DB`
- `RABBITMQ_HOST`, `RABBITMQ_PORT`, `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_VHOST` (если `ENABLE_CELERY=1`/`ENABLE_EVENTS=1`)
- хосты внешних сервисов, которые реально используются: `DJANGO_HOST`, `DOCUMENTATION_HOST`, `EAV_HOST`, `CHECKLIST_HOST`, `WORKFLOWS_HOST`, `PLANNING_HOST`
- при `SYNC_RESOURCE_ID=1``GATEWAY_URL`, `RESOURCE_URL`
- `TRACING_USE=False` (иначе — `TRACING_HOST`, `TRACING_SERVICE_NAME`)
- для воркера — `FLOWS_DB_*`, `ISSUES_DB_*`, `RFI_DB_*`, `MAILGUN_*` или SMTP
Готовые значения-примеры для всех переменных приведены в `.env.example`.

142
apps/flows/ENDPOINTS.md Normal file
View File

@ -0,0 +1,142 @@
# Эндпоинты, с которыми взаимодействует flows-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `flows-frontend`).
## Как устроено взаимодействие
В отличие от единого реестра эндпоинтов, запросы во `flows-frontend` выполняются **точечно** из MobX-сторов (`module/store/stores/*.ts`) через общий HTTP-клиент `httpService`.
`httpService` создаётся в `module/api/http-service.ts` фабрикой `createHttpService` из `@sarex-team/sdk-js`. Клиент предоставляет методы `getRequest`, `postRequest`, `putRequest`, `patchRequest`, `deleteRequest`, каждый из которых принимает объект вида:
```ts
httpService.getRequest({
service: "flows", // логическое имя сервиса (ключ из hosts.ts)
url: `/flows/${id}/?full=true`, // путь запроса относительно базового хоста сервиса
data: { ... }, // тело запроса (для post/put/patch)
// ...прочие опции axios
});
```
Базовый хост сервиса подставляется по ключу `service` из `module/api/hosts.ts` в зависимости от `BUILD_ENV`. Итоговый URL = `<базовый хост сервиса>` + `url`.
Окружение выбирается переменной `BUILD_ENV` (`module/env.js`): одно из `local`, `stage`, `prod`, `preprod`, `contour`, `severstal`, `uralchem`. В `webpack.config.js` значение прокидывается в бандл через `DefinePlugin`. Значение по умолчанию при резолве хоста — `prod`.
Подключаемый удалённый модуль (`documentations`) описан отдельно в `module/api/modules-hosts.ts` и резолвится функцией `getModuleHost` (Module Federation, `remoteEntry.js`).
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Показаны `stage` и `prod`; дополнительно определены `local`, `preprod` и `contour``contour` — относительные пути для изолированного контура; в `local` сервис `sarex` проксируется на `/sarex-backend`).
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `flows` | Сервис процессов согласования (flows, reviews, steps, statuses) | `https://stage-api.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` |
| `sarex` | Локальный backend (Django `core`/`client`) | `""` (относительные пути) | `""` |
| `gateway_api_v1` | Gateway API v1 (ресурсы) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` |
| `gateway_api_v2` | Gateway API v2 (пользователи) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` |
| `documentations` | Сервис документации (диски, документы) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` |
| `eav_api_v0` | Сервис EAV (атрибуты/схемы) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` |
| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` |
| `transmittals` | Сервис передачи документации (шаблоны) | `https://stage-api.sarex.io/transmittals/api/v1` | `https://api.sarex.io/transmittals/api/v1` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
Удалённые модули (`module/api/modules-hosts.ts`):
| Модуль | `stage` | `prod` |
| --- | --- | --- |
| `documentations` | `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js` | `https://modules.sarex.io/documentations/static/module/remoteEntry.js` |
## Эндпоинты по сервисам
### `flows` — Сервис процессов согласования
Источник: `module/store/stores/processes.ts`, `module/store/stores/resources.ts`.
| Метод (`*Request`) | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/flows/{id}/?full=true` | `processes.loadFlow` | Маршрут по id (с шагами/статусами) |
| POST | `/flows/filter/` | `processes` (фильтр) | Список маршрутов по фильтру |
| POST | `/flows/count_flows_by_resource/` | `processes` | Количество маршрутов по ресурсам |
| POST | `/flows/count_flows_by_resource/` | `processes.getCountByResourceId` | Количество маршрутов для одного ресурса |
| POST | `/flows/` | `processes.createFlow` | Создать маршрут |
| POST | `/flows/{id}/copy/?full=true` | `processes` (копирование) | Копировать маршрут |
| PUT | `/flows/{id}/?full=true` | `processes` (обновление) | Обновить маршрут |
| PATCH | `/flows/bulk-update/` | `processes.bulkUpdateFlow` | Массовое обновление маршрутов |
| POST | `/steps/` | `processes.createStep` | Создать шаг |
| PUT | `/steps/{id}/?full=true` | `processes.updateStep` | Обновить шаг |
| PATCH | `/steps/{stepId}/update_reviewers/` | `processes` | Обновить согласующих шага |
| GET | `/steps/{id}/active_reviews/` | `processes.getActiveReviewsForReviewer` | Активные review на шаге |
| POST | `/statuses/` | `processes.createStatus` | Создать статус |
| DELETE | `/statuses/{id}/` | `processes.deleteStatus` | Удалить статус |
| POST | `/reviews/count_by_resource_id/` | `resources` | Количество review по ресурсу |
### `sarex` — Локальный backend (Django)
Источник: `module/store/stores/users.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/api/client/settings/` | `users.getCurrentUser` | Настройки/данные текущего пользователя |
| GET | `/api/core/users/?company={id}&{query}` | `users.getUsers` / `getUsersByCompanyId` / `getAllUsersByCompanyId` | Пользователи компании |
| GET | `/api/core/admin/departments/?{query}` | `users.fetchDepartmentsSA` | Департаменты (service account) |
| GET | `/api/core/admin/departments/?company={id}&{query}` | `users.fetchDepartmentsByCompanyId` | Департаменты компании |
| GET | `/api/core/admin/positions/?{query}` | `users.fetchPositionsSA` | Должности (service account) |
| GET | `/api/core/admin/positions/?company={id}&{query}` | `users.fetchPositionsByCompanyId` | Должности компании |
### `gateway_api_v1` — Gateway API v1 (ресурсы)
Источник: `module/store/stores/resources.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/resources/?{query}` | `resources` | Список ресурсов (по фильтру) |
| GET | `/resources/?company_id={id}` | `resources` | Ресурсы компании |
### `gateway_api_v2` — Gateway API v2 (пользователи)
Источник: `module/store/stores/users.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/users/?{query}` | `users.getUsersByResourceId` | Пользователи по ресурсу |
### `documentations` — Сервис документации
Источник: `module/store/stores/documents.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/disks/{id}/documents` | `documents.fetchDocumentsByDiskId` | Документы диска |
### `eav_api_v0` — Сервис EAV (атрибуты)
Источник: `module/store/stores/attributes.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| GET | `/schema/?model_name=flow&company_id={id}` | `attributes.fetchFlowsAttributes` | Схема атрибутов для модели `flow` |
### `checklists` — Сервис чек-листов
Источник: `module/store/stores/checklists.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| POST | `/checklists/filter/` | `checklists.fetchChecklists` | Список чек-листов по фильтру |
### `transmittals` — Сервис передачи документации
Источник: `module/store/stores/transmittals.ts`.
| Метод | Путь | Стор / метод | Назначение |
| --- | --- | --- | --- |
| POST | `/transmittal_templates` | `transmittals` | Шаблоны трансмитталов (пагинация по `next`, фильтр по `resources`) |
### `zitadel` — IdP
Хост определён в `hosts.ts` для аутентификации через SDK; прямых вызовов из сторов в текущей версии модуля нет (используется инфраструктурой `@sarex-team/sdk-js`).
## Замечания
- Единого файла-реестра эндпоинтов (`endpoints.ts`) во `flows-frontend` нет — вызовы разбросаны по сторам `module/store/stores/*`. При добавлении нового запроса указывайте `service` строго из ключей `hosts.ts`.
- Часть путей содержит завершающий слэш и query-параметры прямо в строке `url` (напр. `/flows/{id}/?full=true`) — это соответствует поведению backend (`flows-backend`), где роуты объявлены со слэшем на конце.
- Сервис `sarex` в `stage`/`prod` имеет пустой базовый хост (`""`), то есть запросы идут по относительным путям того же origin; в `local` он проксируется на `/sarex-backend`, в `contour` — на относительные пути контура.

1736
apps/flows/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

75
apps/iam/.env.example Normal file
View File

@ -0,0 +1,75 @@
# iams-v2 — пример переменных окружения.
# Конфиг читается из окружения (github.com/sethvargo/go-envconfig).
# Перед разбором подхватывается env-файл через godotenv: путь из ENV_FILE, иначе ".env".
# Значения ниже — дефолты из кода и примеры для локального запуска.
# App
ENVIRONMENT=local
LOG_LEVEL=info
SERVICE_NAME=iams
SERVICE_VERSION=0.0.0
# HTTP (prefix HTTP_)
HTTP_PORT=8080
# ReadBufferSize (Fiber/fasthttp), байты
HTTP_READ_BUFFER_SIZE=131072
# Database (prefix DB_)
DB_DSN=postgres://postgres:secret@localhost:5432/postgres?sslmode=disable
DB_MIGRATIONS_PATH=migrations
# Auth — внешний контур /external/api/* (prefix AUTH_)
# false ⇒ группа /external/api не регистрируется, JWT не разбирается
AUTH_ENABLED=false
# Sonyflake — machine id для генерации resource.id (prefix SONYFLAKE_)
SONYFLAKE_MACHINE_ID=1
# Zitadel Management API (prefix ZITADEL_)
# при ENABLED=true обязательны HOST, ACCESS_TOKEN, ORG_RULES_FILE
ZITADEL_ENABLED=false
ZITADEL_HOST=
ZITADEL_ACCESS_TOKEN=
ZITADEL_ORG_RULES_FILE=config/zitadel/org-rules-stage.json
# S3 (presigned GET для вложений виджета) (prefix S3_)
# при ENABLED=true обязательны ENDPOINT_URL, BUCKET_NAME, ACCESS_KEY_ID, SECRET_ACCESS_KEY
S3_ENABLED=false
S3_ENDPOINT_URL=https://storage.yandexcloud.net
S3_BUCKET_NAME=
S3_REGION=ru-central1
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_PRESIGN_EXPIRES=1h
# Kafka (prefix KAFKA_)
KAFKA_ENABLED=false
KAFKA_BROKERS=localhost:9092
KAFKA_SECURITY_PROTOCOL=PLAINTEXT
KAFKA_SASL_MECHANISM=
KAFKA_SASL_PLAIN_USERNAME=
KAFKA_SASL_PLAIN_PASSWORD=
KAFKA_SSL_CAFILE=
# Политика имён топиков (prefix KAFKA_TOPIC_)
# По умолчанию topic = event_type. PREFIX добавляется ко всем именам.
KAFKA_TOPIC_PREFIX=
# Путь к JSON-файлу {event_type: topic} с переопределениями
KAFKA_TOPIC_OVERRIDES_FILE=
# Legacy-топик (BrokerMessage пользователя)
KAFKA_TOPIC_LEGACY_AMS_SYNC=ams-sync
# OpenTelemetry (prefix OTEL_)
OTEL_ENABLED=false
OTEL_HOST=localhost
OTEL_PORT=4317
OTEL_INSECURE=true
OTEL_SERVICE_NAME=iams
# Переменная выбора env-файла (читается до разбора конфига)
# ENV_FILE=.env
# --- Вспомогательное (не читается конфигом приложения) ---
# DSN для интеграционных тестов (общий контейнер Postgres): см. Makefile/README
# TEST_DB_DSN=postgres://postgres:secret@localhost:5432/postgres?sslmode=disable
# Включение Kafka в docker compose для сервиса http
# COMPOSE_KAFKA_ENABLED=false

174
apps/iam/CONFIGURATION.md Normal file
View File

@ -0,0 +1,174 @@
# Конфигурация проекта iams-v2
Документ описывает все переменные окружения и способы конфигурирования сервиса **iams** (`platform/iams-v2`) — сервиса управления пользователями, ресурсами и правами доступа (IAM).
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `internal/app/config.go` (функция `app.Load`) через библиотеку [`github.com/sethvargo/go-envconfig`](https://github.com/sethvargo/go-envconfig) (структура `Config`).
Особенности разбора:
- вложенные секции задаются полями-структурами с тегом `env:", prefix=<PREFIX>_"`, напр. `DB_``Config.DB`, `S3_``Config.S3`;
- у большинства полей задан дефолт через `env:"NAME, default=..."`; поля без дефолта при отсутствии остаются нулевыми, а обязательность проверяется отдельными валидаторами (см. ниже);
- перед разбором окружения подхватывается env-файл через [`godotenv`](https://github.com/joho/godotenv): путь берётся из переменной `ENV_FILE`, иначе `.env`. Отсутствие файла **не** является ошибкой (ошибка `godotenv.Load` игнорируется), реальные значения читаются из окружения процесса.
Отдельного конфиг-файла (yaml/toml) у приложения нет, за исключением двух внешних файлов, путь к которым задаётся переменными: `ZITADEL_ORG_RULES_FILE` (JSON-правила организаций Zitadel) и `KAFKA_TOPIC_OVERRIDES_FILE` (JSON-переопределения имён топиков).
Валидация на старте (`app.Load`):
- при `S3_ENABLED=true` обязательны `S3_ENDPOINT_URL`, `S3_BUCKET_NAME`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` — иначе ошибка старта;
- при `ZITADEL_ENABLED=true` обязательны `ZITADEL_HOST`, `ZITADEL_ACCESS_TOKEN`, `ZITADEL_ORG_RULES_FILE` — иначе ошибка старта;
- при заданном `KAFKA_TOPIC_OVERRIDES_FILE` файл читается и парсится как JSON `{event_type: topic}` — при ошибке чтения/парсинга сервис не стартует.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Переменные окружения процесса + env-файл (`.env` или `ENV_FILE`), который автоматически загружается `godotenv` |
| Локально (docker compose) | `docker-compose.yml`: сервис `http` получает `DB_DSN`, `KAFKA_ENABLED`, `S3_ENABLED` из окружения хоста; Postgres — из блока `environment` |
| Kubernetes (Helm) | `.helm/values.yaml` (universal-chart): блоки `envs` (обычные значения по окружениям `_default`/`stage`/`production`), `secretEnvs` (значения из k8s-секретов) и `volumes` (монтирование CA-сертификата Kafka) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переключение стенда по ветке/тегу (`workflow.rules`), общие шаблоны из `generic/common-ci`, `HELM_SET_ARGS` |
Способы запуска процессов (`cmd/*`):
| Команда | Точка входа | Назначение |
| --- | --- | --- |
| `server` (docker `ENTRYPOINT`, `make build`) | `cmd/server/main.go` | HTTP API (Fiber). Флаг `-env-file=PATH` переопределяет `ENV_FILE` |
| `cli migrate` | `cmd/cli/main.go` | Прогон миграций БД (`golang-migrate`). Требует `DB_DSN`; путь миграций — `DB_MIGRATIONS_PATH` |
| `seed` | `cmd/seed/main.go` | Наполнение БД тестовыми данными (флаги `--seed`, `--resources`, `--users`, `--tenant-id`, `--type-id`) |
Docker-образ (`docker/httpserver/Dockerfile`) собирает статический бинарник `server` (scratch-образ) и копирует каталог `config/` (файлы правил Zitadel). Миграции применяются отдельной командой `cli migrate` (в локальном сценарии — целью `make postgres-up`).
## Переменные приложения
В столбце «Переменная» указано полное имя (с учётом префикса секции). Дефолт `—` означает, что в коде значения по умолчанию нет (поле остаётся нулевым, если не задано).
### App (верхний уровень, без префикса)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENVIRONMENT` | string | `local` | Окружение развёртывания (`local`/`stage`/`prod`) |
| `LOG_LEVEL` | string | `info` | Уровень логирования (`debug`/`info`/`warn`/`error`) |
| `SERVICE_NAME` | string | `iams` | Имя сервиса |
| `SERVICE_VERSION` | string | `0.0.0` | Версия сервиса |
### HTTP (`HTTP_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HTTP_PORT` | string | `8080` | Порт HTTP-сервера |
| `HTTP_READ_BUFFER_SIZE` | int | `131072` | Размер буфера чтения запроса (Fiber/fasthttp), байты |
### Database (`DB_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DB_DSN` | string | — | DSN PostgreSQL (`postgres://user:pass@host:port/db?sslmode=...`). Обязателен для работы БД и команды `cli migrate` |
| `DB_MIGRATIONS_PATH` | string | `migrations` | Путь к каталогу SQL-миграций |
### Auth (`AUTH_*`)
Конфигурация внешнего контура `/external/api/*`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AUTH_ENABLED` | bool | `false` | При `false` внешний контур не регистрируется и пользовательская авторизация не выполняется; внутренние эндпоинты работают без изменений |
### Sonyflake (`SONYFLAKE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SONYFLAKE_MACHINE_ID` | uint16 | `1` | Machine ID генератора идентификаторов `resource.id` для новых записей |
### Zitadel (`ZITADEL_*`)
Management API (Service User с правами администратора). При `ENABLED=true` `HOST`, `ACCESS_TOKEN`, `ORG_RULES_FILE` обязательны.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ZITADEL_ENABLED` | bool | `false` | Включить интеграцию с Zitadel |
| `ZITADEL_HOST` | string | — | Хост Zitadel (напр. `https://login.sarex.io`) |
| `ZITADEL_ACCESS_TOKEN` | string | — | Access token сервисного пользователя |
| `ZITADEL_ORG_RULES_FILE` | string | — | Путь к JSON-файлу правил организаций (`config/zitadel/org-rules-<env>.json`) |
### S3 (`S3_*`)
Presigned GET для вложений виджета в приватном S3-совместимом бакете. При `ENABLED=true` `ENDPOINT_URL`, `BUCKET_NAME`, `ACCESS_KEY_ID`, `SECRET_ACCESS_KEY` обязательны.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_ENABLED` | bool | `false` | Включить S3-интеграцию |
| `S3_ENDPOINT_URL` | string | — | Эндпоинт S3 (напр. `https://storage.yandexcloud.net`) |
| `S3_BUCKET_NAME` | string | — | Имя бакета |
| `S3_REGION` | string | `ru-central1` | Регион |
| `S3_ACCESS_KEY_ID` | string | — | Access key |
| `S3_SECRET_ACCESS_KEY` | string | — | Secret key |
| `S3_PRESIGN_EXPIRES` | duration | `1h` | Срок жизни presigned-ссылки (Go duration, напр. `1h`, `15m`) |
### Kafka (`KAFKA_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_ENABLED` | bool | `false` | Включить Kafka-продюсер |
| `KAFKA_BROKERS` | string | `localhost:9092` | Список брокеров |
| `KAFKA_SECURITY_PROTOCOL` | string | `PLAINTEXT` | Протокол (`PLAINTEXT`/`SASL_SSL`/...) |
| `KAFKA_SASL_MECHANISM` | string | — | SASL-механизм (напр. `SCRAM-SHA-512`) |
| `KAFKA_SASL_PLAIN_USERNAME` | string | — | SASL-логин |
| `KAFKA_SASL_PLAIN_PASSWORD` | string | — | SASL-пароль |
| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату для SSL |
Политика имён топиков (`KAFKA_TOPIC_*`):
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_TOPIC_PREFIX` | string | — | Префикс, добавляемый ко всем именам топиков. По умолчанию `topic = event_type` |
| `KAFKA_TOPIC_OVERRIDES_FILE` | string | — | Путь к JSON-файлу `{event_type: topic}` с переопределениями. Читается на старте |
| `KAFKA_TOPIC_LEGACY_AMS_SYNC` | string | `ams-sync` | Legacy-топик для `BrokerMessage` пользователя |
### OpenTelemetry (`OTEL_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `OTEL_ENABLED` | bool | `false` | Включить трейсинг |
| `OTEL_HOST` | string | `localhost` | Хост OTLP/gRPC-коллектора |
| `OTEL_PORT` | string | `4317` | Порт коллектора |
| `OTEL_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору |
| `OTEL_SERVICE_NAME` | string | `iams` | Имя сервиса в трейсах |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт — `universal-chart` (сервис `iams`). Обычные значения задаются в блоке `envs` с профилями `_default`/`stage`/`production` и содержат те же переменные `ENVIRONMENT`, `LOG_LEVEL`, `AUTH_ENABLED`, `HTTP_*`, `DB_MIGRATIONS_PATH`, `ZITADEL_*`, `S3_*`, `KAFKA_*`, `OTEL_*` (различаются адресами БД/брокеров/коллектора, бакетом, доменом Zitadel и путём к файлу правил).
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `DB_DSN` | `iams-secret` | `db-dsn` |
| `ZITADEL_ACCESS_TOKEN` | `iams-secret` | `zitadel-access-token` |
| `KAFKA_SASL_PLAIN_USERNAME` | `iams-secret` | `kafka-sasl-plain-username` |
| `KAFKA_SASL_PLAIN_PASSWORD` | `iams-secret` | `kafka-sasl-plain-password` |
| `S3_ACCESS_KEY_ID` | `yc-s3-secret` | `key_id` |
| `S3_SECRET_ACCESS_KEY` | `yc-s3-secret` | `access_key` |
Помимо env, чарт монтирует CA-сертификат Kafka из секрета `ya-ca-secret` как файл `/etc/ca-certificates/Yandex/ca-cert` — именно на него указывает `KAFKA_SSL_CAFILE` в конфигурации стенда/прода.
Прочие значения чарта (не переменные приложения): `deployment.*` (имя, порт `8080`, `replicaCount`: `_default=1`, `production=4`), `image.name` (`cr.yandex/.../iams:latest`), `service.*` (порт/targetPort `8080`).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | Chart version |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `platform` | `0.0.1-stage` |
| тег (`CI_COMMIT_TAG`) | `production` | `iam` | `0.0.1-prod` |
| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Ключевые переменные пайплайна: `SERVICE_NAME=iams`, `DOCKERFILE_PATH=./docker/httpserver/Dockerfile`, `RELEASE_NAME`, `CHART_NAME`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, ссылки на GitLab). Джобы `lint` (`go vet` + `golangci-lint`) и `test` (`go test ./...`) выполняются на merge request.
## Замечания и минимальный набор для локального запуска
- Приложение **загружает env-файл автоматически** (`godotenv`): достаточно положить `.env` рядом с бинарником или указать путь через `ENV_FILE` / флаг `-env-file`. Отсутствие файла не является ошибкой.
- Три интеграции выключены по умолчанию (`ENABLED=false`): `AUTH`, `ZITADEL`, `S3`, `KAFKA`, `OTEL`. Включение любой из `S3`/`ZITADEL` требует заполнения обязательных полей (см. валидаторы выше), иначе сервис не стартует.
- Для локального запуска минимально необходимо задать `DB_DSN` (Postgres поднимается через `docker compose up -d postgres`, миграции — `make postgres-up``cli migrate`). Остальные секции можно оставить с дефолтами (`ENABLED=false`).
- Готовые значения-примеры для всех переменных приведены в `.env.example`.

207
apps/iam/ENDPOINTS.md Normal file
View File

@ -0,0 +1,207 @@
# Эндпоинты сервиса iams-v2
Документ описывает все HTTP-эндпоинты, которые **предоставляет** сервис `iams` (IAM: пользователи, ресурсы/проекты, права доступа). В отличие от фронтенд-модулей, здесь описан контракт самого сервиса.
## Как устроено взаимодействие
Сервер — Fiber (`internal/controller/http/server.go`). Маршруты регистрируются двумя наборами:
- **Внутренний контур** (`RegisterAPIRoutes`) — без аутентификации на уровне приложения, доступ ограничивается сетевым слоем (не публикуется через ingress). Базовые группы: `/api/v0`, `/api/admin/v0`, `/api/v1`, `/api/v2`.
- **Внешний контур** (`RegisterExternalAPIRoutes`) — регистрируется только при `AUTH_ENABLED=true`. Перед маршрутами выполняется разбор JWT (`AuthMiddleware`) и пометка контекста (`ExternalAPIMiddleware`, для админ-группы дополнительно `ExternalUserAdminAPIMiddleware`). Базовые группы: `/external/api/v0`, `/external/api/admin/v0`, `/external/api/v1`, `/external/api/v2`. Внешний контур публикует **подмножество** внутренних маршрутов, часть — только на чтение.
Глобальные middleware: `requestid`, `recover`, OTel (при `OTEL_ENABLED` и заданном `OTEL_SERVICE_NAME`), логирование.
### Аутентификация (внешний контур)
Токен передаётся заголовком `Authorization: Bearer <jwt>` (`internal/controller/http/jwt/parser.go`). **Подпись токена не проверяется** — доверие делегируется вышестоящему gateway/virtual service. Схема разбора выбирается по заголовку `Identity`:
- заголовок `Identity` отсутствует/пуст → legacy-формат (Django SimpleJWT);
- заголовок `Identity` задан → Zitadel (клейм `urn:zitadel:iam:user:metadata`, значения полей — base64).
### Формат ответов и ошибок
Успешные ответы — JSON (списки: `{ count, results }`; часть эндпоинтов возвращает объект напрямую). Ошибки — JSON `{ "error": "...", "id": "..." }` (`httpx.Response`). Доменный `Kind` маппится на HTTP-статус (`httpx/error_resolve.go`): `InvalidArgument/OutOfRange`→`400`, `Unauthenticated`→`401`, `PermissionDenied`→`403`, `NotFound`→`404`, `Aborted/AlreadyExists`→`409`, `PreconditionFailed`→`412`, `ResourceExhausted`→`429`, `Unavailable`→`503`, `Internal/Unknown/DataLoss`→`500`. Идентификатор запроса — заголовок из `requestid`.
### Пагинация и фильтры
Списки — через query `limit`/`offset` (по умолчанию `limit=1000`). Дополнительные фильтры задаются query-параметрами (напр. для `/resource`: `parent_id`, `type`, `tenant_id`, `name`, `code`, `public_id`, `target_id`, `service_accounts`; для `/users`: `username`, `search`, `is_active`, `is_staff`, `is_superuser`, `service_account_id`, `id`, `company_id`, `departments`, `positions`, `groups`).
## Служебные эндпоинты
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/health` | Health-check: проверка БД и (при включении) Kafka. `200` — ok, `503` — недоступность зависимости |
## Внутренний контур
### Users (`/api/v0/users`, `/api/admin/v0`, `/api/v1`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/users` | Список пользователей (фильтры, пагинация) |
| POST | `/api/v0/users` | Создать пользователя (если включён use case) |
| PATCH | `/api/v0/users` | Массовое обновление пользователей (legacy bulk_update) |
| GET | `/api/v0/users/:id` | Пользователь по id |
| PUT | `/api/v0/users/:id` | Полное обновление пользователя |
| PATCH | `/api/v0/users/:id` | Частичное обновление пользователя |
| DELETE | `/api/v0/users/:id` | Удалить пользователя |
| POST | `/api/admin/v0/users/activation` | Активация/деактивация пользователей компании |
| POST | `/api/v1/users-with-resources` | Пользователи с их ресурсами (тело: `tenant_id`, `id[]`) |
| GET | `/api/v1/users-grouped-by-resource/` | Пользователи, сгруппированные по ресурсу (если включён use case) |
### Permission check (`/api/v0`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/api/v0/permissions-check` | Проверка набора прав пользователя (`user_id`, `checks[]`) |
### Groups (`/api/v0/groups`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/groups` | Список групп |
| POST | `/api/v0/groups` | Создать группу |
| POST | `/api/v0/groups/search` | Поиск групп (пагинация, сортировка, фильтры) |
| GET | `/api/v0/groups/:id` | Группа по id |
| PUT | `/api/v0/groups/:id` | Обновить группу |
| PATCH | `/api/v0/groups/:id` | Частично обновить группу |
| DELETE | `/api/v0/groups/:id` | Удалить группу |
### Positions (`/api/v0/positions`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/positions` | Список должностей |
| POST | `/api/v0/positions` | Создать должность |
| GET | `/api/v0/positions/:id` | Должность по id |
| PUT | `/api/v0/positions/:id` | Обновить должность |
| PATCH | `/api/v0/positions/:id` | Частично обновить должность |
| DELETE | `/api/v0/positions/:id` | Удалить должность |
### Departments (`/api/v0/departments`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/departments` | Список подразделений |
| POST | `/api/v0/departments` | Создать подразделение |
| GET | `/api/v0/departments/:id` | Подразделение по id |
| PUT | `/api/v0/departments/:id` | Обновить подразделение |
| PATCH | `/api/v0/departments/:id` | Частично обновить подразделение |
| DELETE | `/api/v0/departments/:id` | Удалить подразделение |
### Django permissions (`/api/v0/permissions`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/permissions` | Список permission'ов |
| POST | `/api/v0/permissions` | Создать permission |
| POST | `/api/v0/permissions/search` | Поиск permission'ов |
| GET | `/api/v0/permissions/:id` | Permission по id |
| DELETE | `/api/v0/permissions/:id` | Удалить permission |
### Resource types v0 (`/api/v0/resource-types`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/resource-types` | Список типов ресурсов |
| POST | `/api/v0/resource-types` | Создать тип |
| GET | `/api/v0/resource-types/:id` | Тип по id |
| PUT | `/api/v0/resource-types/:id` | Обновить тип |
| PATCH | `/api/v0/resource-types/:id` | Частично обновить тип |
| DELETE | `/api/v0/resource-types/:id` | Удалить тип |
### Resources v0 (`/api/v0/resources`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/resources` | Список ресурсов (legacy v0) |
| POST | `/api/v0/resources` | Создать ресурс |
| GET | `/api/v0/resources/:id` | Ресурс по id |
| PUT | `/api/v0/resources/:id` | Обновить ресурс |
| PATCH | `/api/v0/resources/:id` | Частично обновить ресурс |
| DELETE | `/api/v0/resources/:id` | Удалить ресурс |
### Projects v0 (`/api/v0/projects`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/projects` | Список проектов |
| POST | `/api/v0/projects` | Создать проект |
| GET | `/api/v0/projects/:publicID` | Проект по public id |
| PUT | `/api/v0/projects/:publicID` | Обновить проект |
| PATCH | `/api/v0/projects/:publicID` | Частично обновить проект |
| DELETE | `/api/v0/projects/:publicID` | Удалить проект |
### Widgets v0 (`/api/v0/widgets`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/widgets` | Список виджетов |
| POST | `/api/v0/widgets` | Создать виджет |
| GET | `/api/v0/widgets/:publicID` | Виджет по public id |
| PUT | `/api/v0/widgets/:publicID` | Обновить виджет |
| PATCH | `/api/v0/widgets/:publicID` | Частично обновить виджет |
| DELETE | `/api/v0/widgets/:publicID` | Удалить виджет |
### Resources v1 (`/api/v1/resource`, `/api/v1/targets`, `/api/v1/service-accounts`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v1/resource` | Список ресурсов (v1) |
| POST | `/api/v1/resource` | Создать ресурс |
| GET | `/api/v1/resource/:publicID` | Ресурс по public id |
| PUT | `/api/v1/resource/:publicID` | Обновить ресурс |
| PATCH | `/api/v1/resource/:publicID` | Частично обновить ресурс |
| DELETE | `/api/v1/resource/:publicID` | Удалить ресурс |
| GET | `/api/v1/targets/:target_id/resource` | Ресурс по target id (только внутренний) |
| GET | `/api/v1/resources-grouped-by-sa` | Ресурсы, сгруппированные по сервисному аккаунту (если включено) |
| GET | `/api/v1/service-accounts` | Список сервисных аккаунтов |
### Widgets v2 (`/api/v2/resource`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v2/resource` | Список виджетов (v2) |
| POST | `/api/v2/resource` | Создать виджет |
| GET | `/api/v2/resource/:publicID` | Виджет по public id |
| PUT | `/api/v2/resource/:publicID` | Обновить виджет |
| PATCH | `/api/v2/resource/:publicID` | Частично обновить виджет |
| DELETE | `/api/v2/resource/:publicID` | Удалить виджет |
### Permissions v1 (`/api/v1`)
| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v1/resource_permission` | Список прав на ресурсы |
| POST | `/api/v1/resource_permission` | Создать права (пакет `items[]`) |
| GET | `/api/v1/resource_permission/:id` | Право по id |
| DELETE | `/api/v1/resource_permission/:id` | Удалить право |
| PATCH | `/api/v1/permissions-bulk` | Массовое изменение прав (`tenant_id`, `permissions`) |
| GET | `/api/v1/company_resource_permission` | Список прав компании на ресурсы |
| POST | `/api/v1/company_resource_permission` | Создать права компании (пакет `items[]`) |
| GET | `/api/v1/company_resource_permission/:id` | Право компании по id |
| DELETE | `/api/v1/company_resource_permission/:id` | Удалить право компании |
## Внешний контур (`/external/api/*`, только при `AUTH_ENABLED=true`)
Публикуется подмножество внутренних маршрутов; часть — только на чтение. Пути идентичны внутренним, но с префиксом `/external/api`.
| Группа | Маршруты | Отличия от внутреннего контура |
| --- | --- | --- |
| `/external/api/v0/users` | `GET /`, `GET /:id`, `PATCH /` | Только чтение + массовый PATCH (bulk) |
| `/external/api/admin/v0/users` | `POST /activation`, полный CRUD `/users` | Полный доступ (админ-группа) |
| `/external/api/admin/v0/positions` | `/positions` (CRUD) | Как внутренний |
| `/external/api/admin/v0/departments` | `/departments` (CRUD) | Как внутренний |
| `/external/api/admin/v0/groups` | `/groups` (CRUD + search) | Как внутренний |
| `/external/api/admin/v0/permissions` | `/permissions` (django) | Как внутренний |
| `/external/api/v0/resource-types` | `/resource-types` (CRUD) | Как внутренний |
| `/external/api/v0/resources` | `/resources` (CRUD, v0) | Как внутренний |
| `/external/api/v0/projects` | `GET /`, `GET /:publicID` | Только чтение |
| `/external/api/v0/widgets` | `/widgets` (CRUD) | Как внутренний |
| `/external/api/v1/resource` | `GET /`, `GET /:publicID` | Только чтение |
| `/external/api/v2/resource` | `/resource` (виджеты v2, CRUD) | Как внутренний |
| `/external/api/v1/resource_permission` | CRUD-подмножество | Как внутренний |
| `/external/api/v1/permissions-bulk` | `PATCH /` | Как внутренний |
| `/external/api/v1/company_resource_permission` | CRUD-подмножество | Как внутренний |
> Часть маршрутов регистрируется условно — только если соответствующий хендлер/use case подключён при инициализации сервера (проверки `Has*` в хендлерах). При отсутствии сервиса группа не регистрируется.

1170
apps/iam/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
# Общие настройки процесса
PYTHONPATH=src
PICCOLO_CONF=db.config
# App
DEBUG=true
SERVICE_URL=https://stage.sarex.io
# HTTP App
HTTP_APP_HOST=0.0.0.0
HTTP_APP_PORT=8000
HTTP_APP_ROOT_PATH=""
HTTP_APP_WORKERS=1
HTTP_APP_ADMIN_ENABLE=true
# Database
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
# Kafka
KAFKA_HOST=host
KAFKA_USERNAME=username
KAFKA_PASSWORD=password
# Если задан KAFKA_SSL_CERT (для http-приложения) или KAFKA_SSL_CAFILE (для kafka-consumer) —
# подключение идёт по SASL-SCRAM-SHA-512 поверх TLS, иначе без SSL.
KAFKA_SSL_CERT=""
KAFKA_SSL_CAFILE=ssl_cafile
KAFKA_EAV_ASSETS_TOPIC=eav_assets_topic
# OpenTelemetry
OTEL_ENABLE=false
OTEL_URL=http://signoz-otel-collector-external.signoz.svc.cluster.local:4317
OTEL_SERVICE_NAME=inspections-backend.inspections-stage
OTEL_INSECURE=true
# Auth (JWT)
# Если false — middleware аутентификации не подключается, используется дефолтный пользователь.
JWT_AUTH_ENABLE=false
# Notifications
NOTIFICATIONS_ENABLE=false
NOTIFICATIONS_EMAIL_FROM=hello@sarex.io
# Sarex backend
SAREX_BACKEND_URL=https://stage.sarex.io
SAREX_BACKEND_TIMEOUT=30
# base64(login:password)
SAREX_BACKEND_AUTH=base64(login:password)
# EAV (сервис атрибутов)
EAV_URL=https://stage-api.sarex.io/eav
EAV_TIMEOUT=30
# Workflows (процессы / отправка email)
WORKFLOWS_URL=http://workflows-api-service.processing-stage
WORKFLOWS_TIMEOUT=30
WORKFLOWS_EMAIL_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/notification:email
# Мобильное приложение (версии)
MOBILE_APP_CURRENT_VERSION=1.0.0
MOBILE_APP_RECOMMENDED_VERSION=1.0.0
MOBILE_APP_REQUIRED_VERSION=1.0.0

View File

@ -0,0 +1,216 @@
# Конфигурация проекта inspections-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) (класс `Config` и вложенные `*Config`).
Особенности разбора:
- у приложения **нет единого общего префикса** — каждая секция задаётся собственным `env_prefix` в своём `SettingsConfigDict` (напр. `HTTP_APP_`, `DATABASE_`, `KAFKA_`, `OTEL_`, `JWT_AUTH_`, `NOTIFICATIONS_`, `SAREX_BACKEND_`, `EAV_`, `WORKFLOWS_`, `MOBILE_APP_`);
- две переменные верхнего уровня (`DEBUG`, `SERVICE_URL`) читаются без префикса;
- вложенности через разделитель нет — плоские имена вида `<PREFIX><FIELD>`, напр. `DATABASE_HOST``database.host`;
- почти у всех полей есть значения по умолчанию, поэтому отсутствие переменной обычно не приводит к ошибке старта — берётся дефолт из кода. В таблицах ниже приведены дефолты из `src/config.py`.
Отдельного конфиг-файла (yaml/toml) у приложения нет. Часовой пояс приложения зафиксирован в коде: `app_timezone = ZoneInfo("Europe/Moscow")`.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (uv) | `Makefile` через `SET_ENV` делает `set -a; source .env; set +a` перед запуском команд. Шаблон переменных — `.env.template` (в репозитории; `.env*` в `.gitignore`, кроме `.env.template`) |
| Docker | `docker/http/Dockerfile` и `docker/kafka/Dockerfile` фиксируют `PYTHONPATH=src` и `PICCOLO_CONF=db.config`; прикладные переменные пробрасываются рантаймом (k8s) |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `api` и `kafka-app` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`), сборка образа и деплой универсального чарта |
Способы запуска процессов:
| Команда (`Makefile`) | Точка входа | Назначение |
| --- | --- | --- |
| `make run` | `src/cmd/http/main.py` | HTTP API (uvicorn, `app.http:create_app`, `factory=True`) |
| — (kafka-consumer) | `src/cmd/kafka/main.py` | Обработчик Kafka-событий (FastStream) |
| `make migrate` | `piccolo migrations forwards all` | Применение миграций Piccolo |
| `make migrations` | `piccolo migrations new inspections --auto` | Генерация новой миграции |
| `make sync_eav` | `src/cmd/scripts/sync_eav.py` | Разовая синхронизация EAV |
| `make playground` | `piccolo playground run` | Локальный playground Piccolo (sqlite) |
| `make format` / `make format-check` | `ruff` | Форматирование/проверка кода |
Порядок запуска в контейнере HTTP (`docker/http/entrypoint.sh`): сначала выполняются миграции (`piccolo migrations forwards all`), затем стартует `src/cmd/http/main.py`. Контейнер kafka-consumer (`docker/kafka/entrypoint.sh`) запускает только `src/cmd/kafka/main.py` (миграции не применяет).
## Переменные приложения
В столбце «Переменная» указано полное имя (префикс + поле). Дефолт `—` означает, что значения по умолчанию у поля нет.
### App (верхний уровень)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DEBUG` | bool | `True` | Режим отладки: логирование SQL-запросов Piccolo, `reload=True` у uvicorn, непродакшн-режим админки |
| `SERVICE_URL` | string | `https://stage.sarex.io` | Внешний базовый URL Sarex, используется при формировании ссылок (уведомления, экспорт) |
### HTTP App (`HTTP_APP_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HTTP_APP_HOST` | string | `0.0.0.0` | Адрес прослушивания |
| `HTTP_APP_PORT` | int | `8000` | Порт |
| `HTTP_APP_ROOT_PATH` | string | `""` | Root path (префикс за реверс-прокси, напр. `/inspections`) |
| `HTTP_APP_WORKERS` | int | `1` | Число воркеров uvicorn |
| `HTTP_APP_ADMIN_ENABLE` | bool | `True` | Монтировать ли Piccolo-admin по пути `/admin/` |
### Database (`DATABASE_*`)
PostgreSQL через Piccolo (`PostgresEngine`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DATABASE_HOST` | string | `postgres` | Хост PostgreSQL |
| `DATABASE_PORT` | int | `5432` | Порт PostgreSQL |
| `DATABASE_NAME` | string | `postgres` | Имя базы данных |
| `DATABASE_USER` | string | `postgres` | Пользователь БД |
| `DATABASE_PASSWORD` | string | `postgres` | Пароль пользователя БД |
### Kafka (`KAFKA_*`)
Используется FastStream (`KafkaBroker`). Продюсер — в HTTP-приложении (публикация событий инспекций), консьюмер — отдельный процесс (`kafka-app`), подписан на топик EAV-ассетов.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_HOST` | string | `""` | Bootstrap-сервер (`bootstrap_servers=[host]`) |
| `KAFKA_USERNAME` | string | `""` | Пользователь (SASL) |
| `KAFKA_PASSWORD` | string | `""` | Пароль (SASL) |
| `KAFKA_SSL_CERT` | string | `""` | CA-сертификат как строка (`cadata`). Если задан — HTTP-приложение подключается по `SASLScram512` поверх TLS, иначе без SSL |
| `KAFKA_SSL_CAFILE` | string | `""` | Путь к CA-файлу (`cafile`). Если задан — kafka-consumer подключается по `SASLScram512` поверх TLS, иначе без SSL |
| `KAFKA_EAV_ASSETS_TOPIC` | string | `""` | Топик событий EAV-ассетов, на который подписан consumer |
> Механизм безопасности отличается между процессами: HTTP-приложение (`app/http.py`) смотрит на `KAFKA_SSL_CERT` (строка сертификата), а kafka-consumer (`app/kafka.py`) — на `KAFKA_SSL_CAFILE` (путь к файлу).
### OpenTelemetry (`OTEL_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `OTEL_ENABLE` | bool | `False` | Включить трейсинг/структурированное логирование. При `True` отключается `access_log` uvicorn |
| `OTEL_URL` | string | `http://signoz-otel-collector-external.signoz.svc.cluster.local:4317` | Адрес OTLP-коллектора |
| `OTEL_SERVICE_NAME` | string | `inspections-backend.inspections-stage` | Имя сервиса в трейсах |
| `OTEL_INSECURE` | bool | `True` | Небезопасное (без TLS) подключение к коллектору |
### Auth (`JWT_AUTH_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `JWT_AUTH_ENABLE` | bool | `False` | Подключать ли middleware `TokenUserMiddleware`. При `False` используется дефолтный пользователь из `entity/context.py` (для локальной разработки) |
> Middleware декодирует JWT **без проверки подписи** (`verify_signature: False`). При наличии заголовка `identity` полезная нагрузка берётся из него (метаданные Zitadel), иначе — из `authorization`. Роуты `/docs/`, `/openapi.json/`, `/admin/`, `/internal/` исключены из проверки.
### Notifications (`NOTIFICATIONS_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `NOTIFICATIONS_ENABLE` | bool | `False` | Включить отправку уведомлений об инспекциях |
| `NOTIFICATIONS_EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя писем |
### Sarex backend (`SAREX_BACKEND_*`)
HTTP-клиент основного бэкенда Sarex (данные пользователей и т.п.).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SAREX_BACKEND_URL` | string | `https://stage.sarex.io` | Базовый URL |
| `SAREX_BACKEND_TIMEOUT` | int | `30` | Таймаут запроса (сек) |
| `SAREX_BACKEND_AUTH` | string (base64) | `base64(login:password)` | Basic-auth в виде base64(`login:password`) |
### EAV (`EAV_*`)
HTTP-клиент сервиса атрибутов (EAV).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `EAV_URL` | string | `https://stage-api.sarex.io/eav` | Базовый URL |
| `EAV_TIMEOUT` | int | `30` | Таймаут запроса (сек) |
### Workflows (`WORKFLOWS_*`)
HTTP-клиент сервиса процессов; используется, в т.ч. для запуска отправки email.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOWS_URL` | string | `http://workflows-api-service.processing-stage` | Базовый URL |
| `WORKFLOWS_TIMEOUT` | int | `30` | Таймаут запроса (сек) |
| `WORKFLOWS_EMAIL_DOCKER_IMAGE` | string | `cr.yandex/crp3ccidau046kdj8g9q/notification:email` | Docker-образ шага отправки email |
### Mobile App (`MOBILE_APP_*`)
Значения отдаются эндпоинтом `GET /api/v1/mobile-app/version/`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `MOBILE_APP_CURRENT_VERSION` | string | `""` | Текущая версия |
| `MOBILE_APP_RECOMMENDED_VERSION` | string | `""` | Рекомендуемая версия |
| `MOBILE_APP_REQUIRED_VERSION` | string | `""` | Минимально требуемая версия |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения через `pydantic-settings`, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `PYTHONPATH=src` | `.env.template`, `Dockerfile` | Корень пакета приложения |
| `PICCOLO_CONF=db.config` | `.env.template`, `Dockerfile` | Модуль конфигурации Piccolo (`APP_REGISTRY`, `DB`, `ADMIN_ASGI_APP`) |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт — обёртка над зависимостью `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`). Описаны два сервиса: `api` (HTTP) и `kafka-app` (consumer), у каждого свои блоки `envs` и `secretEnvs`. Значения различаются по окружениям через ключи `_default`/`stage`/`preprod`/`production`.
Обычные значения (`envs`) содержат те же переменные приложения, что и выше (различаются `SERVICE_URL`, `EAV_URL`, `WORKFLOWS_URL`, `OTEL_*`, `KAFKA_EAV_ASSETS_TOPIC`, `HTTP_APP_ROOT_PATH=/inspections`, `HTTP_APP_WORKERS=3`, `JWT_AUTH_ENABLE=true`, `NOTIFICATIONS_ENABLE` и т.п.). Отличия от дефолтов кода: в проде `DEBUG=false`, `OTEL_ENABLE=true`, `JWT_AUTH_ENABLE=true`.
Значения из секретов (`secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`_default` / `stage`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `DATABASE_USER` | `ya-pg-secret` / `inspections-postgresql-secret` | `username` |
| `DATABASE_PORT` | `ya-pg-secret` / `inspections-postgresql-secret` | `port` |
| `DATABASE_NAME` | `ya-pg-secret` / `inspections-postgresql-secret` | `database` |
| `DATABASE_HOST` | `ya-pg-secret` / `inspections-postgresql-secret` | `host` |
| `DATABASE_PASSWORD` | `ya-pg-secret` / `inspections-postgresql-secret` | `password` |
| `KAFKA_HOST` | `yc-kafka-secret` / `inspections-kafka-secret` | `host` |
| `KAFKA_USERNAME` | `yc-kafka-secret` / `inspections-kafka-secret` | `username` |
| `KAFKA_PASSWORD` | `yc-kafka-secret` / `inspections-kafka-secret` | `password` |
| `KAFKA_SSL_CERT` | `inspections-kafka-secret` | `cert` |
| `SAREX_BACKEND_AUTH` | `sarex-backend-auth-secret` | `key` |
Помимо env, у сервиса `kafka-app` смонтирован CA-сертификат Yandex как файл `/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt` (секрет `yc-ch-certificate`, ключ `certificate`) — на него указывает `KAFKA_SSL_CAFILE`.
Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, probes), `image.*`, `service.*`, `imagePullSecrets`, у `kafka-app``command` (`python src/cmd/kafka/main.py`) и `volumes`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | Release / Chart |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `proc` | `inspections-backend` |
| ветка `master` | `preprod` | `inspections-preprod` | `inspections-backend` |
| тег (`CI_COMMIT_TAG`) | `production` | `inspections-prod` | `sarex-inspections` |
| `merge_request_event` | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Ключевые переменные пайплайна: `SERVICE_NAME=inspections-backend`, `DOCKERFILE_PATH=./docker/http/Dockerfile`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (проброс `image.name`, `global.env`, метаданных коммита для сервисов `api` и `kafka-app`). Job `lint` прогоняет `ruff check` и `ruff format --check` на образе `uv 0.7.13 / python3.13`.
## Замечания
- Приложение **не загружает `.env` автоматически** — переменные экспортируются в окружение (в `Makefile` — через `set -a; source .env; set +a`, в k8s — через env-блоки чарта).
- Аутентификация в middleware декодирует JWT **без проверки подписи**; безопасность обеспечивается сетевым слоем/ingress. При `JWT_AUTH_ENABLE=false` активен захардкоженный дефолтный пользователь (`entity/context.py`), пригодный только для локальной разработки.
- Большинство полей конфигурации имеют дефолты — при отсутствии переменной сервис стартует со значением из кода. Для реального окружения значения задаются в `.helm/values.yaml`.
- Для экспорта поддерживается единственный формат — `xlsx` (`InspectionExportType`).
## Минимальный набор для локального запуска
Скопировать `.env.template` в `.env`, поднять PostgreSQL и (при необходимости обработки событий) Kafka, применить миграции (`make migrate`) и запустить API (`make run`). Минимально стоит задать:
- `DEBUG`, `SERVICE_URL`
- `HTTP_APP_HOST`, `HTTP_APP_PORT`
- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`
- `JWT_AUTH_ENABLE=false` (для дебага без токена)
- `KAFKA_*` (если нужен consumer/публикация событий)
- `SAREX_BACKEND_*`, `EAV_*`, `WORKFLOWS_*` (для интеграций)
- `OTEL_ENABLE=false` локально

View File

@ -0,0 +1,115 @@
# Эндпоинты, с которыми взаимодействует inspections-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `inspections-frontend`).
## Как устроено взаимодействие
Запросы выполняются через общий `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). У сервиса есть методы `getRequest`, `postRequest`, `patchRequest`, `deleteRequest`, каждый из которых принимает объект с полями:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (дописывается к базовому хосту сервиса);
- `data` — тело запроса (для POST/PATCH);
- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer` и т.п.);
- `showErrorNotification` — показывать ли уведомление об ошибке.
Базовый хост подставляется по паре «`service` + окружение». Окружение определяется глобальной переменной сборки `BUILD_ENV` (`local`/`stage`/`preprod`/`prod`/`contour`); при её отсутствии используется `prod` (`const buildEnv = BUILD_ENV ?? "prod"`). В режиме `local` для http-сервиса выставляется `type: "original"`. `BUILD_ENV` задаётся при сборке (напр. `BUILD_ENV=stage npm start`) и прокидывается через webpack DefinePlugin.
Определения запросов сгруппированы по файлам в `module/api/` (`inspections.ts`, `assets.ts`, `Issues.ts`, `premises-api.ts`) и по стор-файлам в `module/Inspections/store/` (`inspections.ts`, `filters.ts`, `resource.ts`, `calendar.ts`, `issuesStore.ts`).
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url` запроса.
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `inspections` | Сервис событий/инспекций (этот бэкенд) | `https://stage-api.sarex.io/inspections/api/v1` | `https://api.sarex.io/inspections/api/v1` |
| `sarexApi` | Gateway/API Sarex (`/gateway`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` |
| `eavV0` | Сервис атрибутов EAV, API v0 | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` |
| `eavV4` | Сервис атрибутов EAV, API v4 (ассеты) | `https://stage-api.sarex.io/eav/api/v4` | `https://api.sarex.io/eav/api/v4` |
| `issues` | Сервис замечаний | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` |
| `premises` | Сервис помещений | `https://stage-api.sarex.io/premises/api/v1` | `https://api.sarex.io/premises/api/v1` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определены окружения `local`, `preprod` и `contour`. В `local` сервис `sarex` проксируется на `/sarex-backend`, а `inspections`/`eav`/`issues`/`premises` указывают на стейдж. В `contour` все хосты пустые (относительные пути для изолированного контура). Сервис `zitadel` в hosts объявлен, но прямых запросов из модуля к нему нет — аутентификация обрабатывается на уровне SDK/платформы.
## Эндпоинты по сервисам
### `inspections` — Сервис событий/инспекций
Базовый хост уже включает `/api/v1`, поэтому в путях ниже он не повторяется.
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| GET | `/inspections/` | `store/resource.ts` | Создание/список событий |
| POST | `/inspections/` | `store/resource.ts` | Создать событие |
| POST | `/inspections/filter/` | `store/resource.ts`, `store/calendar.ts` | Список событий с фильтрами и пагинацией в теле |
| GET | `/inspections/{id}/` | `api/inspections.ts`, `store/resource.ts` | Событие по id |
| PATCH | `/inspections/{id}/` | `api/inspections.ts` | Частичное обновление события |
| GET | `/inspections/types/` | `api/inspections.ts` | Типы событий компании (query `company_id`) |
| POST | `/inspections/status-count/` | `store/inspections.ts` | Счётчики по статусам (фильтры в теле) |
| POST | `/inspections/filter-options/` | `store/filters.ts` | Доступные значения фильтров |
| GET | `/inspections/aggregate/created_at/minmax/` | `store/filters.ts` | Мин/макс по дате создания |
| GET | `/inspections/aggregate/inspection_dt/minmax/` | `store/filters.ts` | Мин/макс по дате проведения |
| GET | `/inspections/change-history/` | `api/inspections.ts` | История изменений (пагинация, фильтры в query) |
| GET | `/inspections/export/` | `store/inspections.ts` | Экспорт событий (xlsx) |
| POST | `/inspections/unavailable-dates/` | `api/inspections.ts` | Недоступные даты для исполнителей |
| POST | `/inspections/unavailable-users/` | `api/inspections.ts` | Недоступные исполнители на интервал |
### `sarexApi` — Gateway/API Sarex
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| GET | `/gateway/api/v1/resources/?company_id={companyId}` | `store/inspections.ts` | Ресурсы (проекты) компании |
| GET | `/gateway/api/v2/users/` | `store/issuesStore.ts`, `store/resource.ts` | Пользователи (с пагинацией/фильтрами) |
| GET | `/gateway/api/v1/attachments/?company_id={companyId}&instance_id={id}&model_name=inspection` | `store/resource.ts` | Вложения события |
| POST | `/gateway/api/v1/attachments/` | `store/resource.ts` | Создать вложение |
| DELETE | `/gateway/api/v1/attachments/{id}` | `store/resource.ts` | Удалить вложение |
### `sarex` — Локальный сервис данных
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| GET | `/api/client/settings/` | `store/inspections.ts` | Клиентские настройки |
| GET | `/api/core/users/` | `store/inspections.ts` | Пользователи |
| GET | `/api/core/admin/departments/?company={companyId}` | `store/inspections.ts` | Отделы компании |
| GET | `/api/core/admin/positions/?company={companyId}` | `store/inspections.ts` | Должности компании |
### `eavV4` — Сервис атрибутов EAV (ассеты)
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| POST | `/assets/search/` | `api/assets.ts` | Поиск ассетов по списку id |
| GET | `/assets/` | `api/assets.ts` | Список ассетов (фильтры/пагинация/теги в query) |
> Теги ассетов (`EnumAssetTags`): `location`, `project_structure`, `events.can_be_selected`, `remarks.can_be_selected`.
### `eavV0` — Сервис атрибутов EAV (атрибуты)
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| GET | `/attribute/?company_id={companyId}` | `store/inspections.ts` | Атрибуты компании |
### `issues` — Сервис замечаний
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| POST | `/issues/` | `api/Issues.ts` | Создать замечание |
| GET | `/issues/` | `api/Issues.ts` | Список замечаний (фильтры по компании/ресурсу/событию/типам) |
| POST | `/attachments/` | `api/Issues.ts`, `store/issuesStore.ts` | Прикрепить медиа к замечанию |
| GET | `/companies/{companyId}/status-model/` | `api/Issues.ts` | Статусная модель замечаний компании |
| GET | `/issue-types/` | `api/Issues.ts` | Типы замечаний компании |
### `premises` — Сервис помещений
Базовый хост уже включает `/api/v1`.
| Метод | Путь | Где вызывается | Назначение |
| --- | --- | --- | --- |
| GET | `/premises/{id}/` | `api/premises-api.ts` | Помещение по id |
| POST | `/premises/filter/` | `api/premises-api.ts` | Помещения по фильтру (пагинация в query) |
| POST | `/premise_types/filter/` | `api/premises-api.ts` | Типы помещений по фильтру (пагинация в query) |
## Обработка ошибок
Показ уведомлений об ошибках управляется флагом `showErrorNotification` в параметрах запроса (включается точечно для части запросов). Для сериализации query-параметров-массивов местами используется `query-string` с `arrayFormat: "comma"` (напр. в `api/Issues.ts`). Часть запросов в `api/assets.ts` оборачивает ошибку в `throw new Error(...)`.

File diff suppressed because it is too large Load Diff

101
apps/issues/.env.example Normal file
View File

@ -0,0 +1,101 @@
# Django
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_ADMIN_SECRET_KEY=''
DJANGO_TOKEN=django-token
# Environment
ENVIRONMENT=production
ENVIRONMENT_CLIENT=stage
# Database (PostgreSQL)
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=password
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
# Sarex auth (basic)
SAREX_USERNAME=
SAREX_PASSWORD=
# External services
AERO_HOST=https://stage.sarex.io
AERO_PUBLIC_HOST=https://stage.sarex.io
BASE_AERO_URL=https://stage.sarex.io
BASE_AUTH_URL=https://stage.sarex.io
SAREX_API=https://stage.sarex.io
SAREX_HOST=https://stage.sarex.io
SERVICE_URL=https://stage.sarex.io
GATEWAY_URL=https://stage-api.sarex.io/gateway
DOCUMENTATIONS_URL=http://documentations-api-svc.documentations.svc.cluster.local:8000
WORKFLOWS_URL=http://workflows-api-service.platform.svc.cluster.local:8000
WORKFLOWS_HOST=http://workflows-api-service.platform.svc.cluster.local:8000
RESOURCES_API_HOST=http://iams.platform.svc.cluster.local:8080
REVIEW_HOST=https://stage-api.sarex.io/flows
INSPECTION_HOST=https://stage-api.sarex.io/inspections
EAV_HOST=http://eav-service.eav-stage
# RabbitMQ / Celery broker
RABBITMQ_USERNAME=mcc
RABBITMQ_PASSWORD=mcc
RABBITMQ_HOSTNAME=rabbitmq-service
RABBITMQ_VHOST=api
# Redis (Celery result backend)
REDIS_HOST=redis
REDIS_DB=0
# Kafka
KAFKA_HOST=
KAFKA_USERNAME=
KAFKA_PASSWORD=
KAFKA_SSL_CAFILE=/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt
KAFKA_EAV_ASSETS_TOPIC=assets-broadcast-test
KAFKA_ISSUES_TOPIC=issues-broadcast
# S3 (Yandex Cloud) — общий бакет
YC_S3_ACCESS_KEY_ID=
YC_S3_SECRET_ACCESS_KEY=
YC_S3_BUCKET_NAME=
YC_S3_ENDPOINT_URL=
YC_S3_VERIFY=true
# S3 — бакет предписаний
PRESCRIPTION_S3_ACCESS_KEY_ID=
PRESCRIPTION_S3_SECRET_ACCESS_KEY=
PRESCRIPTION_S3_BUCKET=
PRESCRIPTION_S3_ENDPOINT_URL=
# Email
ENABLE_MAILGUN=True
EMAIL_FROM=hello@sarex.io
EMAIL_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/notification:email
MAILGUN_BASE_URL=https://api.mailgun.net/v3/mg.sarex.io
MAILGUN_API_KEY=
USE_NOTIFICATIONS=True
# SMTP (альтернатива Mailgun)
SMTP_HOST=
SMTP_PORT=
# Prescriptions workflow
PRESCRIPTION_WF_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/rendering-template:develop
PRESCRIPTION_WF_RESULT_PATH=prescriptions-storage-stage
PRESCRIPTION_WF_CALLBACK=cr.yandex/crp3ccidau046kdj8g9q/webhook-caller:develop
PRESCRIPTION_INTERNAL_HOST=http://issues-backend-service.proc.svc.cluster.local:80/internal
DOCX_TO_PDF_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/docx-to-pdf:latest
# Export
EXPORT_WF_CROPPING_DOCKER_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/crop-issue-pin-area:stage
# OpenTelemetry
USE_OTEL=False
SERVICE_NAME=issues-backend.sarex-issues
TRACER_ENDPOINT=localhost:4375
USE_INSECURE=True
MODULE=issues
TEAM=proc_team
COMPONENT=backend
# uWSGI / infra
API_ADDRESS=8000
SENTRY_KEY=

View File

@ -0,0 +1,245 @@
# Конфигурация проекта issues-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса замечаний (Issues).
## Способы конфигурирования
Сервис настраивается **через переменные окружения**. Это Django-приложение; настройки читаются в `src/config/settings/base.py` и `src/config/settings/production.py` напрямую через `os.getenv(...)`. В начале `base.py` вызывается `load_dotenv()` ([`python-dotenv`](https://pypi.org/project/python-dotenv/)), поэтому при локальном запуске файл `.env` из рабочего каталога **подхватывается автоматически**.
Активный модуль настроек задаётся переменной `DJANGO_SETTINGS_MODULE` (в контейнере/Helm — `config.settings.production`) либо флагом `--settings=config.settings.production` у `manage.py`. Модуль `production.py` импортирует всё из `base.py` и переопределяет `DEBUG=False`, `ALLOWED_HOSTS`, `SIMPLE_JWT`, `LOGGING` и часть внешних хостов.
Отдельного конфиг-файла (yaml/toml) у приложения нет. Дополнительно на инфраструктурном уровне используются `config/settings/base.py` для Celery/Kafka/OTel и Helm-чарт для задания переменных в Kubernetes.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально | Переменные окружения процесса и файл `.env` (грузится `load_dotenv()` в `base.py`) |
| Локально (Kafka) | `docker compose --file local-kafka-docker-compose.yml up -d` поднимает брокер; консьюмер — `manage.py consume_kafka` |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для каждого сервиса (`api`, `celery`, `celery-beat`, `kafka-app`) |
| CI/CD (GitLab) | `.gitlab-ci.yml`: общий шаблон `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); окружение выбирается по ветке/тегу |
Способы запуска процессов:
| Процесс | Команда | Назначение |
| --- | --- | --- |
| HTTP API | `uwsgi` (entrypoint) / `manage.py runserver` | REST API (DRF), OpenAPI-схема через drf-spectacular |
| Celery worker | `celery -A config worker -l info -E --concurrency=2` | Обработчик фоновых задач (`issues.tasks`, `issues.notifications`, `prescriptions.tasks`) |
| Celery beat | `celery -A config beat -l info` | Периодические задачи (ежедневный инкремент счётчиков, отчёт о просрочках) |
| Kafka consumer | `python3 run_kafka_app.py` / `manage.py consume_kafka` | Консьюмер Kafka (топики ассетов и замечаний) |
Порядок старта в контейнере задаётся `compose/server/entrypoint.sh` (миграции + запуск uWSGI по `compose/server/uwsgi.ini`). Базовый образ — `python:3.10-slim-bookworm` (`compose/server/Dockerfile`).
## Переменные приложения
Дефолт `—` означает, что явного значения по умолчанию в коде нет (`os.getenv` вернёт `None`); для корректной работы переменную нужно задать.
### Django и окружение
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_SETTINGS_MODULE` | string | `config.settings.production` (Helm) | Модуль настроек Django |
| `DJANGO_ADMIN_SECRET_KEY` | string | `''` | `SECRET_KEY` Django |
| `DJANGO_TOKEN` | string | `django-token` | Служебный токен |
| `ENVIRONMENT` | string | `production` | Окружение развёртывания (в т.ч. атрибут OTel) |
| `ENVIRONMENT_CLIENT` | string | `production` | Клиентское окружение (`stage`/`preprod`/`production`) |
### База данных (PostgreSQL)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DATABASE_NAME` | string | — | Имя базы данных |
| `DATABASE_USER` | string | — | Пользователь БД |
| `DATABASE_PASSWORD` | string | — | Пароль пользователя БД |
| `DATABASE_HOST` | string | — | Хост PostgreSQL |
| `DATABASE_PORT` | int | — | Порт PostgreSQL |
> Движок — `django.db.backends.postgresql`. В Kubernetes значения приходят из секрета (`issues-postgresql-secret` для stage, `ya-pg-secret` для preprod/production), CA-сертификат монтируется как `/root/.postgresql/ca.crt`.
### Внешние сервисы (URL)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SAREX_API` | string | — | Базовый API Sarex (`SAREX_HOST` по умолчанию равен ему) |
| `SAREX_HOST` | string | `= SAREX_API` | Хост Sarex |
| `AERO_HOST` | string | `https://stage.sarex.io` | Хост Aero |
| `AERO_PUBLIC_HOST` | string | `https://stage.sarex.io``production.py` — из env) | Публичный хост Aero |
| `BASE_AERO_URL` | string | `https://lk.sarex.io` | Базовый URL Aero |
| `BASE_AUTH_URL` | string | `https://lk.sarex.io` | Базовый URL аутентификации |
| `SERVICE_URL` | string | `https://lk.sarex.io` | URL сервиса |
| `GATEWAY_URL` | string | `https://lk.sarex.io` | URL gateway |
| `DOCUMENTATIONS_URL` | string | `https://lk.sarex.io` | URL сервиса документаций |
| `WORKFLOWS_URL` | string | `https://lk.sarex.io` | URL сервиса workflows |
| `WORKFLOWS_HOST` | string | `https://lk.sarex.io` | Хост workflows |
| `RESOURCES_API_HOST` | string | `https://lk.sarex.io``production.py``http://sarex-resources-service.resources-prod`) | Хост сервиса ресурсов (IAM) |
| `REVIEW_HOST` | string | `https://lk.sarex.io` | Хост сервиса review/flows |
| `INSPECTION_HOST` | string | `https://lk.sarex.io` | Хост сервиса инспекций |
| `EAV_HOST` | string | `http://eav-service.eav-stage` | Хост сервиса атрибутов (EAV) |
| `SAREX_USERNAME` | string | — | Логин для basic-auth Sarex |
| `SAREX_PASSWORD` | string | — | Пароль для basic-auth Sarex |
### RabbitMQ и Celery
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `RABBITMQ_USERNAME` | string | `mcc` | Пользователь брокера |
| `RABBITMQ_PASSWORD` | string | `mcc` | Пароль брокера |
| `RABBITMQ_HOSTNAME` | string | `rabbitmq-service` | Хост брокера |
| `RABBITMQ_VHOST` | string | `api` | Виртуальный хост |
| `REDIS_HOST` | string | `redis` | Хост Redis (result backend) |
| `REDIS_DB` | int | `0` | Номер БД Redis |
> `CELERY_BROKER_URL` собирается как `amqp://{user}:{password}@{hostname}/{vhost}` + `?heartbeat=30`. `CELERY_RESULT_BACKEND``redis://{REDIS_HOST}:6379/{REDIS_DB}`. Расписание beat: инкремент счётчиков `1:00`, отчёт о просрочках `6:00` (`Europe/Moscow`).
### Kafka
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_HOST` | string | — | Адрес брокера Kafka |
| `KAFKA_USERNAME` | string | — | Пользователь |
| `KAFKA_PASSWORD` | string | — | Пароль |
| `KAFKA_SSL_CAFILE` | string | — | Путь к CA-сертификату (в Helm — YandexInternalRootCA) |
| `KAFKA_EAV_ASSETS_TOPIC` | string | — | Топик трансляции ассетов (EAV) |
| `KAFKA_ISSUES_TOPIC` | string | — | Топик трансляции замечаний |
### S3 (Yandex Cloud)
Основное хранилище (`django-storages`, `S3Boto3Storage`):
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `YC_S3_ACCESS_KEY_ID` | string | — | Access key |
| `YC_S3_SECRET_ACCESS_KEY` | string | — | Secret key |
| `YC_S3_BUCKET_NAME` | string | — | Имя бакета |
| `YC_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 |
| `YC_S3_VERIFY` | bool | `None` | Проверять TLS-сертификат (`"true"` → `True`) |
Хранилище предписаний (отдельный бакет):
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PRESCRIPTION_S3_ACCESS_KEY_ID` | string | — | Access key |
| `PRESCRIPTION_S3_SECRET_ACCESS_KEY` | string | — | Secret key |
| `PRESCRIPTION_S3_BUCKET` | string | — | Имя бакета |
| `PRESCRIPTION_S3_ENDPOINT_URL` | string | — | Эндпоинт S3 |
### Почта (Mailgun / SMTP)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_MAILGUN` | bool | `True` | Использовать Mailgun |
| `MAILGUN_BASE_URL` | string | `https://api.mailgun.net/v3/mg.sarex.io` | URL API Mailgun |
| `MAILGUN_API_KEY` | string | (задан дефолт в коде) | API-ключ Mailgun (в проде — из секрета) |
| `EMAIL_FROM` | string | `hello@sarex.io` | Адрес отправителя |
| `EMAIL_DOCKER_IMAGE` | string | `cr.yandex/.../notification:email` | Образ сервиса нотификаций |
| `USE_NOTIFICATIONS` | bool | `True` | Включить отправку уведомлений (`False`/`false`/`0` → выкл.) |
| `SMTP_HOST` | string | `None``production.py``""`) | SMTP-хост (альтернатива Mailgun) |
| `SMTP_PORT` | int | `None` | SMTP-порт |
### Предписания (workflow)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PRESCRIPTION_WF_IMAGE` | string | — | Образ workflow генерации предписаний |
| `DOCX_TO_PDF_IMAGE` | string | — | Образ конвертера DOCX→PDF |
| `PRESCRIPTION_WF_RESULT_PATH` | string | — | Путь/бакет результата |
| `PRESCRIPTION_WF_CALLBACK` | string | — | Образ webhook-caller |
| `PRESCRIPTION_INTERNAL_HOST` | string | — | Внутренний хост колбэков предписаний |
| `EXPORT_WF_CROPPING_DOCKER_IMAGE` | string | `cr.yandex/.../crop-issue-pin-area:prod` | Образ кропа области пина для экспорта |
### OpenTelemetry
Трейсинг подключается только если `USE_OTEL` истинно (`django_otel_tools`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `USE_OTEL` | bool | `False` | Включить трейсинг/логирование через OTel |
| `SERVICE_NAME` | string | `issues-backend.sarex-issues` | Имя сервиса в трейсах |
| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора |
| `USE_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение к коллектору |
| `MODULE` | string | `issues` | Атрибут трейсов |
| `TEAM` | string | `proc_team` | Атрибут трейсов |
| `COMPONENT` | string | `backend` | Атрибут трейсов |
## Переменные инфраструктуры
Не читаются кодом приложения напрямую (или используются вспомогательными компонентами), но участвуют в запуске/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `API_ADDRESS` | Helm (`envs`) | Порт uWSGI (`8000`) |
| `SENTRY_KEY` | Helm (`envs`) | DSN Sentry (задан для stage) |
| `SAREX_MAILER_URL` | Helm (`envs`) | URL сервиса рассылок (`http://mailer-service.mailer:8000`) |
| `MAILGUN_HOST` | Helm (`envs`) | Хост Mailgun на уровне чарта |
| `NPM_TOKEN`, `BUILD_ENV` | CI/Dockerfile | Сборка (актуально для фронтенда) |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт — обёртка над `universal-chart`. Определены четыре сервиса: `api`, `celery`, `celery-beat`, `kafka-app`. Обычные значения (`envs`) задаются для окружений `_default`/`stage`/`preprod`/`production` (различаются адресами БД/сервисов, топиками Kafka, образами, `SERVICE_NAME`, `TRACER_ENDPOINT`).
Значения из секретов (блок `secretEnvs`, монтируются через `secretKeyRef`):
| Переменная | Секрет (stage / preprod-prod) | Ключ |
| --- | --- | --- |
| `KAFKA_USERNAME` | `issues-kafka-secret` / `yc-kafka-secret` | `username` |
| `KAFKA_PASSWORD` | `issues-kafka-secret` / `yc-kafka-secret` | `password` |
| `KAFKA_HOST` | `issues-kafka-secret` / `yc-kafka-secret` | `host` |
| `SAREX_USERNAME` | `sarex-auth` | `username` |
| `SAREX_PASSWORD` | `sarex-auth` | `password` |
| `DATABASE_HOST` | `issues-postgresql-secret` / `ya-pg-secret` | `host` |
| `DATABASE_NAME` | `issues-postgresql-secret` / `ya-pg-secret` | `database` |
| `DATABASE_PORT` | `issues-postgresql-secret` / `ya-pg-secret` | `port` |
| `DATABASE_USER` | `issues-postgresql-secret` / `ya-pg-secret` | `username` |
| `DATABASE_PASSWORD` | `issues-postgresql-secret` / `ya-pg-secret` | `password` |
| `YC_S3_ACCESS_KEY_ID` | `issues-s3-secret` / `yc-s3-secret` | `key_id` |
| `YC_S3_SECRET_ACCESS_KEY` | `issues-s3-secret` / `yc-s3-secret` | `access_key` |
| `YC_S3_BUCKET_NAME` | `issues-s3-secret` / `yc-s3-secret` | `storage_bucket_name` |
| `YC_S3_ENDPOINT_URL` | `issues-s3-secret` / `yc-s3-secret` | `endpoint_url` |
| `RABBITMQ_VHOST` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `vhost` |
| `RABBITMQ_USERNAME` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `user` |
| `RABBITMQ_HOSTNAME` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `host` |
| `RABBITMQ_PASSWORD` | `issues-rabbitmq-secret` / `rabbitmq-secret` | `password` |
| `MAILGUN_API_KEY` | `mailgun-secret` | `api-key` |
| `DJANGO_TOKEN` | `django-secret` | `token` |
| `DJANGO_ADMIN_SECRET_KEY` | `django-admin-secret` | `secret_key` |
| `PRESCRIPTION_S3_ACCESS_KEY_ID` | `prescription-s3-secret` | `key_id` |
| `PRESCRIPTION_S3_SECRET_ACCESS_KEY` | `prescription-s3-secret` | `access_key` |
| `PRESCRIPTION_S3_BUCKET` | `prescription-s3-secret` | `storage_bucket_name` |
| `PRESCRIPTION_S3_ENDPOINT_URL` | `prescription-s3-secret` | `endpoint_url` |
Дополнительно чарт монтирует конфиг uWSGI (`uwsgi-configmap` → `/opt/server/uwsgi.ini`, только сервис `api`), CA-сертификат PostgreSQL (`yc-ch-certificate` → `/root/.postgresql/ca.crt`) и внутренний CA Яндекса (`YandexInternalRootCA.crt`).
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Основные переменные: `SERVICE_NAME=issues`, `DOCKERFILE_PATH=./compose/server/Dockerfile`. Окружение выбирается по ветке/тегу:
| Условие | STAND | Namespace | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `proc` | `0.0.1-stage` |
| ветка `master` | `preprod` | `issues-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `issues-prod` | `0.0.1-prod` |
`HELM_SET_ARGS` для каждого окружения проставляет образы четырёх сервисов (`api`, `celery`, `celery-beat`, `kafka-app`), `universal-chart.global.env` и метаданные коммита (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`).
## Замечания и потенциальные проблемы
- В отличие от FastAPI-сервисов, переменные не имеют единого префикса и читаются напрямую через `os.getenv`. Файл `.env` подхватывается автоматически (`load_dotenv()` в `base.py`).
- `DEBUG` в `base.py` установлен в `True`; в `production.py` переопределяется на `False`. Для боевого окружения обязателен модуль `config.settings.production`.
- `MAILGUN_API_KEY` имеет захардкоженный дефолт в коде — в реальных окружениях его нужно переопределять секретом.
- Ряд переменных без дефолта (`DATABASE_*`, `KAFKA_*`, `YC_S3_*`, `SAREX_USERNAME`/`SAREX_PASSWORD`, `PRESCRIPTION_WF_*`) обязательны для полноценной работы соответствующих подсистем.
- Переменные `SAREX_MAILER_URL`, `MAILGUN_HOST`, `SENTRY_KEY`, `API_ADDRESS` задаются в Helm, но не читаются кодом приложения напрямую.
## Минимальный набор для локального запуска
Postgres, RabbitMQ, Redis и Kafka поднимаются локально; приложение — `python ./src/manage.py runserver --settings=config.settings.production`, консьюмер — `manage.py consume_kafka`. Минимально необходимо задать:
- `DJANGO_ADMIN_SECRET_KEY`
- `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_HOST`, `DATABASE_PORT`
- `RABBITMQ_USERNAME`, `RABBITMQ_PASSWORD`, `RABBITMQ_HOSTNAME`, `RABBITMQ_VHOST`, `REDIS_HOST`
- `KAFKA_HOST`, `KAFKA_USERNAME`, `KAFKA_PASSWORD`, `KAFKA_EAV_ASSETS_TOPIC`, `KAFKA_ISSUES_TOPIC` (для консьюмера)
- `YC_S3_*` (для работы с файлами) и при необходимости `PRESCRIPTION_S3_*`
- внешние URL: `SAREX_API`, `AERO_HOST`, `GATEWAY_URL`, `DOCUMENTATIONS_URL`, `WORKFLOWS_URL`, `RESOURCES_API_HOST`, `EAV_HOST`, `INSPECTION_HOST`, `REVIEW_HOST`
- почта: `ENABLE_MAILGUN` + `MAILGUN_*` **или** `SMTP_HOST`/`SMTP_PORT`
- `USE_OTEL=False` для локальной разработки
Готовые значения-примеры приведены в `.env.example`.

138
apps/issues/ENDPOINTS.md Normal file
View File

@ -0,0 +1,138 @@
# Эндпоинты, с которыми взаимодействует issues-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `issues-frontend`).
## Как устроено взаимодействие
Запросы сгруппированы по доменным API-объектам в каталоге `module/api/` (`IssuesApi`, `CoreApi`, `PrescriptionsApi`, `InspectionsApi`, `AttributesApi`, `AssetsApi`, `ContractsApi`, `ResourcesApi`, `TemplatesApi`, `PremisesApi`, `DocumentationApi`). Каждый метод вызывает единый `httpService` (`module/api/http-service.ts`).
`httpService` создаётся фабрикой `createHttpService` из `@sarex-team/sdk-js` и принимает карту хостов `hosts` (`module/api/hosts.ts`) и текущее окружение `BUILD_ENV`. Вызов задаётся объектом:
- `service` — логическое имя сервиса (ключ из `hosts`, см. таблицу ниже);
- метод — `getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`;
- `url` — путь запроса (дописывается к базовому хосту сервиса);
- `axiosConfig` — доп. настройки axios (`params`, `paramsSerializer`, `responseType` и т.п.);
- `data` — тело запроса;
- `showErrorNotification`, `queryKey`, `cache` — опции показа ошибок, ключа кеша и кеширования.
Итоговый URL = `<базовый хост сервиса для BUILD_ENV>` + `url`. Базовый хост выбирается по `BUILD_ENV` (`local`/`stage`/`prod`/`preprod`/`contour`).
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Приведены `stage` и `prod`; в `contour` используются относительные пути, в `preprod` — домен `api.preprod.sarex.io`, в `local` — как в `stage`, но `sarex` проксируется на `/sarex-backend`.
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `issues` | Сервис замечаний (issues-backend, собственный API) | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` |
| `prescriptions` | Предписания (issues-backend) | `https://stage-api.sarex.io/issues/api/prescriptions` | `https://api.sarex.io/issues/api/prescriptions` |
| `sarexApi` | Gateway/API Sarex (`/gateway`, `/issues`, `/inspections`, `/contracts`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `sarex` | Локальный сервис данных (`/api/core`, `/api/client`) | `""` (относительные пути) | `""` |
| `gateway` | Gateway Sarex | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
| `documentations` | Сервис документации | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` |
| `eav` | Сервис атрибутов/ассетов (EAV) | `https://stage-api.sarex.io/eav/api` | `https://api.sarex.io/eav/api` |
| `files` | Сервис файлов | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` |
| `premises` | Сервис помещений | `https://stage-api.sarex.io/premises/api/v1` | `https://api.sarex.io/premises/api/v1` |
| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` |
| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` |
| `comparisons` | Сервис сравнений | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` |
| `remarks` | Сервис замечаний (remarks) | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` |
| `bim` / `bimv2` | BIM-API | `https://stage-api.sarex.io/bim` (`/bimv2`) | `https://api.sarex.io/bim` (`/bimv2`) |
| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Часть сервисов объявлена в карте хостов, но напрямую в `module/api/*` не вызывается (`workspaces`, `workflows`, `comparisons`, `remarks`, `bim`, `bimv2`, `google`, `zitadel`) — они используются инфраструктурой SDK / другими слоями. Сервис `prescriptions` объявлен в хостах, но методы предписаний фактически ходят через `sarexApi` по пути `/issues/api/prescriptions`.
Подключаемый удалённый модуль `documentations` описан отдельно в `module/api/module-hosts.ts` (`remoteEntry.js` микрофронтенда documentations).
## Эндпоинты по сервисам
### `issues` — Сервис замечаний (собственный API)
Файл `module/api/issuesApi.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `IssuesApi.getStatusModels` | GET | `/companies/{companyId}/status-model/` | Модель статусов компании (по `resource_id`, `issue_type_id`) |
| `IssuesApi.getTypes` | GET | `/issue-types/` | Типы замечаний компании (`company_id`) |
| `IssuesApi.getIssue` | GET | `/issues/{public_id}/` | Замечание по публичному id |
| `IssuesApi.editIssue` | PATCH | `/issues/{publicId}/` | Редактировать замечание |
| `IssuesApi.getChanges` | GET | `/issue-changes/` | История изменений замечания (`issue_id`) |
### `sarexApi` — Gateway/API Sarex
Файлы `issuesApi.ts`, `prescriptionsApi.ts`, `inspections.ts`, `contractsApi.ts`, `attributes.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `IssuesApi.postComment` | POST | `/issues/api/comments/` | Добавить комментарии |
| `IssuesApi.deleteComment` | DELETE | `/issues/api/comments/{id}/` | Удалить комментарий |
| `IssuesApi.postAttachment` | POST | `/issues/api/attachments/` | Загрузить вложение (multipart) |
| `IssuesApi.deleteAttachment` | DELETE | `/issues/api/attachments/{id}/` | Удалить вложение (`issue_public_id`) |
| `PrescriptionsApi.postPrescription` | POST | `/issues/api/prescriptions/` | Создать предписание |
| `PrescriptionsApi.getPrescriptions` | GET | `/issues/api/prescriptions/` | Список предписаний (сериализованные фильтры в query) |
| `InspectionsApi.getInspections` | GET | `/inspections/api/v1/inspections/light/` | Список инспекций (`company_id`, `limit`, `offset`) |
| `InspectionsApi.getInspectionTypes` | GET | `/inspections/api/v1/inspections/types/` | Типы инспекций (`company_id`) |
| `ContractsApi.getContracts` | GET | `/contracts/api/v0/contracts/` | Договоры (`tenant_id`, `contractor_id`, `resource_id`) |
| `AttributesApi.getDocumentAttributes` | GET | `/gateway/api/v1/documents/{documentId}/attributes/` | Атрибуты документа |
### `sarex` — Локальный сервис данных
Файл `module/api/coreApi.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `CoreApi.getUserSettings` | GET | `/api/client/settings/` | Клиентские настройки |
| `CoreApi.getUsers` | GET | `/api/core/users/` | Пользователи компании (`company`, `limit`, `offset`, `show_inactive`) |
| `CoreApi.getDepartments` | GET | `/api/core/admin/departments/` | Отделы компании |
| `CoreApi.getPositions` | GET | `/api/core/admin/positions/` | Должности компании |
### `gateway` — Gateway Sarex
Файлы `coreApi.ts`, `resources.ts`, `templatesApi.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `CoreApi.getGatewayUsers` | GET | `/api/v2/users/` | Пользователи по ресурсу/правам (`resource_id`, `permissions`, `limit`, `offset`) |
| `ResourcesApi.getResourceFullInfo` | GET | `/api/v2/resources/{resourceId}/` | Полная информация о ресурсе |
| `TemplatesApi.getTemplates` | GET | `/api/v1/disks/{diskId}/flat_documents/` | Плоский список документов диска (`type`) |
### `documentations` — Сервис документации
Файлы `documentation.ts`, `templatesApi.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `DocumentationApi.getDocumentById` | GET | `/documents/{docId}` | Документ по id (опц. `extend`) |
| `TemplatesApi.getDisks` | GET | `/disks` | Список дисков |
### `eav` — Атрибуты и ассеты (EAV)
Файлы `assets.ts`, `attributes.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `AssetsApi.getAssetsPost` | POST | `/v4/assets/search/` | Поиск ассетов |
| `AssetsApi.getAssetsGet` | GET | `/v4/assets/` | Список ассетов |
| `AttributesApi.getAttributes` | GET | `/v0/attribute/` | Атрибуты компании (`company_id`) |
### `files` — Сервис файлов
Файл `documentation.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `DocumentationApi.getPage` | GET | `/pages/{sourceId}/{pageId}` | Страница файла (ответ `blob`) |
### `premises` — Сервис помещений
Файл `premises-api.ts`.
| Метод API | HTTP | Путь | Назначение |
| --- | --- | --- | --- |
| `PremisesApi.getPremise` | GET | `/premises/{id}/` | Помещение по id |
| `PremisesApi.getPremisesFilter` | POST | `/premises/filter/` | Фильтрация помещений (`limit`, `offset` в query) |
| `PremisesApi.getPremiseTypesFilter` | POST | `/premise_types/filter/` | Фильтрация типов помещений (`limit`, `offset` в query) |
## Обработка ошибок
Глобальная обработка выполняется в `module/api/http-service.ts`: `httpService` обёрнут в `Proxy`, который для методов запросов (`getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`) перехватывает ошибку и при статусе `403` показывает toast-уведомление (`react-toastify`) с текстом `detail` из ответа либо сообщением «У вас недостаточно прав для выполнения данного действия», после чего пробрасывает ошибку дальше. Для отдельных запросов показ уведомлений включается флагом `showErrorNotification: true`. В окружении `local` SDK переключается в режим `original` (`setSharedHttpServiceConfig({ type: "original" })`).

3278
apps/issues/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

43
apps/mapper/.env.example Normal file
View File

@ -0,0 +1,43 @@
# Mapper (flows mapper) — пример переменных окружения
# Все переменные читаются классами pydantic-settings в src/app/config.py.
# У каждого класса свой env_prefix; вложенного делимитера ("__") НЕТ.
# Значения ниже — дефолты из кода (stage-хосты). Приложение НЕ загружает .env
# автоматически (env_file не задан) — переменные нужно экспортировать в окружение.
# App (класс Settings, без префикса)
API_PREFIX=/api/v1
# Logger (префикс LOG_)
LOG_LEVEL=INFO
LOG_FORMAT='[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s'
# Redis (префикс REDIS_) — кеш ответов внешних сервисов
REDIS_USE=True
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_EXPIRE_DAYS=1
# Documentation service (префикс DOCUMENTATION_)
DOCUMENTATION_HOST=https://stage-api.sarex.io/documentations/api/v1
DOCUMENTATION_TIMEOUT=30
DOCUMENTATION_RETRIES=3
# Flow service (префикс FLOW_)
FLOW_HOST=https://stage-api.sarex.io/flows/api/v1
FLOW_TIMEOUT=30
FLOW_RETRIES=3
# Django / sarex-backend (префикс DJANGO_)
DJANGO_HOST=https://stage.sarex.io/api
DJANGO_TIMEOUT=30
DJANGO_RETRIES=3
# Note service (префикс NOTE_)
NOTE_HOST=https://stage-api.sarex.io/notes/api/v1
NOTE_TIMEOUT=30
NOTE_RETRIES=3
# ВНИМАНИЕ: переменная TIMEOUT (без префикса), которая задаётся в Helm/Kustomize
# как "120", НИ ОДНИМ классом настроек не читается. Реальный таймаут HTTP-клиентов
# берётся из <PREFIX>_TIMEOUT (по умолчанию 30). См. CONFIGURATION.md.

View File

@ -0,0 +1,193 @@
# Конфигурация проекта mapper (flows mapper)
Документ описывает все переменные окружения и способы конфигурирования сервиса `mapper` — mini-HTTP-сервиса, который объединяет (мапит) данные сервисов документации (`documentations`) и процессов (`flows`), а также заметок (`notes`) и Django-бэкенда Sarex.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/app/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/).
В отличие от многих сервисов, здесь **нет единого корневого префикса и нет вложенного делимитера** (`env_nested_delimiter`). Вместо этого каждая секция описана отдельным классом `BaseSettings` со своим `env_prefix` (задаётся во вложенном классе `Config`):
| Класс | `env_prefix` | Секция |
| --- | --- | --- |
| `Settings` | — (без префикса) | Корневые настройки (`API_PREFIX`) |
| `LoggerSettings` | `LOG_` | Логирование |
| `RedisSettings` | `REDIS_` | Кеш Redis |
| `DocumentationSettings` | `DOCUMENTATION_` | HTTP-клиент сервиса документации |
| `FlowSettings` | `FLOW_` | HTTP-клиент сервиса процессов |
| `DjangoSettings` | `DJANGO_` | HTTP-клиент Django-бэкенда |
| `NoteSettings` | `NOTE_` | HTTP-клиент сервиса заметок |
`DocumentationSettings`, `FlowSettings`, `DjangoSettings` и `NoteSettings` наследуются от общего класса `AsyncSessionManager` (поля `host`, `timeout`, `retries`), поэтому у каждого из них одинаковый набор из трёх переменных: `<PREFIX>_HOST`, `<PREFIX>_TIMEOUT`, `<PREFIX>_RETRIES`.
Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически**`config.py` не задан `env_file`, зависимости `python-dotenv` нет) — переменные нужно экспортировать в окружение самому, напр. `set -a && . ./.env && set +a`, либо пробрасывать через контейнер/оркестратор.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (uvicorn/gunicorn) | Переменные окружения процесса. Redis поднимается через `docker-compose.yaml` (только сервис `redis`) |
| Контейнер | `Dockerfile``entrypoint.sh`: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` |
| Kubernetes (Helm, из репозитория сервиса) | `.helm/values.yaml`, блок `universal-chart.services.backend.envs`; деплой из `.gitlab-ci.yml` |
| Kubernetes (GitOps, инфра-репозиторий) | `iac/apps/mapper/*`: Kustomize-база `base/deployment.yaml` (env + секреты Vault) и Flux `HelmRelease` в overlay'ах `brusnika-*` |
Точки входа:
| Команда | Назначение |
| --- | --- |
| `uvicorn main:app` / `python app/main.py` | Локальный запуск (в `main.py` порт `8002`, host `0.0.0.0`) |
| `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` | Прод-запуск (`entrypoint.sh`, порт `8000`) |
Стек: Python 3.10 (`python:3.10-slim-buster`), FastAPI, httpx (асинхронные клиенты), Redis (кеш), PyJWT (разбор токенов).
## Переменные приложения
Дефолт `—` означает, что значение обязательно (иначе ошибка старта). Все дефолты ниже соответствуют коду `config.py`.
### App (класс `Settings`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `API_PREFIX` | string | `/api/v1` | Префикс маршрутов API |
### Logger (`LOG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `INFO` | Уровень логирования (имя уровня `logging`; при неизвестном значении используется `INFO`) |
| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат строки лога |
### Redis (`REDIS_*`)
Кеширует JSON-ответы внешних сервисов (по ключу `"{user_id}_{url}"`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `REDIS_USE` | bool | `True` | Включить кеш. При `True` на старте выполняется `PING` (падение при недоступном Redis) |
| `REDIS_HOST` | string | `localhost` | Хост Redis |
| `REDIS_PORT` | int | `6379` | Порт Redis |
| `REDIS_DB` | int | `0` | Номер базы Redis |
| `REDIS_EXPIRE_DAYS` | int | `1` | TTL записей кеша в днях (в секундах — `expire_days * 24 * 3600`) |
### HTTP-клиенты внешних сервисов
Все четыре клиента наследуют `AsyncSessionManager` (`host`, `timeout`, `retries`). Клиент httpx создаётся с `verify=False` (проверка TLS-сертификата отключена) и транспортом с числом ретраев `retries`. Токены пробрасываются заголовками `Authorization` (всегда) и `Identity` (в режиме Zitadel).
| Секция / префикс | Назначение | Дефолт `HOST` |
| --- | --- | --- |
| `DOCUMENTATION_*` | Сервис документации (диски, документы, бандлы) | `https://stage-api.sarex.io/documentations/api/v1` |
| `FLOW_*` | Сервис процессов (flows, review-данные) | `https://stage-api.sarex.io/flows/api/v1` |
| `DJANGO_*` | Django-бэкенд Sarex (target-links) | `https://stage.sarex.io/api` |
| `NOTE_*` | Сервис заметок (notes) | `https://stage-api.sarex.io/notes/api/v1` |
Для каждого — три переменные (пример для `FLOW`):
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `FLOW_HOST` | string | см. выше | Базовый URL сервиса |
| `FLOW_TIMEOUT` | int | `30` | Таймаут запроса (сек) |
| `FLOW_RETRIES` | int | `3` | Число повторов транспорта httpx |
## Аутентификация
Аутентификация выполняется в `src/app/dependensies.py` (`get_user_data`) на основе заголовков запроса и **без проверки подписи токена** (`jwt.decode(..., options={"verify_signature": False})`). Публичный ключ не используется, отдельных переменных для ключа нет.
| Условие | Режим | Как извлекается `user_id` |
| --- | --- | --- |
| Есть заголовки `Authorization` и `Identity` | `zitadel` | Из payload `Identity`-токена, поле `urn:zitadel:iam:user:metadata.user_id` (base64) |
| Есть только `Authorization` | `sarex` | Из payload основного токена, поле `user_id` |
| Заголовков нет | — | `401 Unauthorized` |
## Переменные инфраструктуры и сборки
Не читаются кодом приложения, но участвуют в сборке/запуске.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`mapper`), используется как `CHART_NAME` |
| `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile |
| `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` | Аргументы сборки / источник триггера (`app`) |
| `IMAGE_NAME`, `CI_COMMIT_SHA`, `CI_PROJECT_URL`, `CI_JOB_URL`, `CI_PROJECT_NAMESPACE` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Прокидываются в universal-chart (образ, commit, ссылки, owner) |
## Переменные из Helm-чарта репозитория сервиса (`.helm/values.yaml`)
Чарт зависит от `universal-chart` (`oci://cr.yandex/.../charts`, версия `0.1.7`). Переменные приложения задаются в блоке `services.backend.envs` с разбивкой по окружениям (`_default`/`stage`/`preprod`/`production`).
| Переменная | `stage` | `preprod` | `production` |
| --- | --- | --- | --- |
| `DOCUMENTATION_HOST` | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.preprod.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` |
| `FLOW_HOST` | `https://stage-api.sarex.io/flows/api/v1` | `https://api.preprod.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` |
| `DJANGO_HOST` | `https://stage.sarex.io/api` | `https://preprod.sarex.io/api` | `https://lk.sarex.io/api` |
| `NOTE_HOST` | `https://stage-api.sarex.io/notes/api/v1` | `https://api.preprod.sarex.io/notes/api/v1` | `https://api.sarex.io/notes/api/v1` |
| `REDIS_USE` | `0` | `0` | `0` |
| `TIMEOUT` | `120` | `120` | `120` |
Прочие параметры чарта (не переменные приложения): `deployment.*` (имя, порт `8000`, `replicaCount` 1/1/3/3, ресурсы, `probes.liveness/readiness`**отключены**), `image.name` (`cr.yandex/.../mapper`), `service.*` (ClusterIP, порт `8000`), `imagePullSecrets` (`dockerhub`), `labels.monitoring=prometheus`.
## Переменные из инфра-репозитория (`iac/apps/mapper`)
GitOps-деплой через Kustomize + Flux, namespace `mapper`. Здесь же лежит настоящий документ.
Структура:
| Путь | Назначение |
| --- | --- |
| `base/` | Базовый Kustomize (`namespace`, `serviceaccount` `mapper-vault`, `deployment`, `service`) |
| `yc-k8s-test/` | Overlay поверх `base` (патч `replicas: 1`) |
| `brusnika-stage/` | Flux `HelmRelease` (universal-chart), хосты `test.sarex.brusnika.tech`, `imagePullSecrets: dockerhub` |
| `brusnika-prod/` | Flux `HelmRelease` (universal-chart), хосты `cde.brusnika.ru`, `imagePullSecrets: regcred` |
Обычные env в `base/deployment.yaml` (production-хосты Sarex):
| Переменная | Значение |
| --- | --- |
| `DOCUMENTATION_HOST` | `https://api.sarex.io/documentations/api/v1` |
| `FLOW_HOST` | `https://api.sarex.io/flows/api/v1` |
| `DJANGO_HOST` | `https://lk.sarex.io/api` |
| `NOTE_HOST` | `https://api.sarex.io/notes/api/v1` |
| `REDIS_USE` | `0` |
| `TIMEOUT` | `120` |
Секреты монтируются через **HashiCorp Vault Agent** (аннотации `vault.hashicorp.com/*`, роль `mapper`) как файлы в `/vault/secrets/*`, которые перед стартом экспортируются в окружение (`set -a && . /vault/secrets/... && set +a`):
| Файл секрета | Источник (Vault path) | Экспортируемые переменные |
| --- | --- | --- |
| `mapper-django-auth` | `secrets/data/vault/common/django_auth` | `MAPPER_DJANGO_TOKEN` |
| `mapper-db` | `secrets/data/postgresql/apps/mapper` | `MAPPER_DB_USER`, `MAPPER_DB_PASSWORD`, `MAPPER_DB_HOST`, `MAPPER_DB_PORT`, `MAPPER_DB_NAME` |
| `mapper-rabbitmq` | `secrets/data/rabbitmq/apps/mapper` | `MAPPER_RABBITMQ_VHOST`, `MAPPER_RABBITMQ_USERNAME`, `MAPPER_RABBITMQ_PASSWORD`, `MAPPER_RABBITMQ_HOST`, `MAPPER_RABBITMQ_PORT` |
| `mapper-s3` | `secrets/data/minio/apps/mapper` | `MAPPER_S3_ENDPOINT`, `MAPPER_S3_REGION`, `MAPPER_S3_BUCKET`, `MAPPER_S3_ACCESS_KEY_ID`, `MAPPER_S3_SECRET_ACCESS_KEY` |
| `mapper-kafka` | `secrets/data/kafka/apps/mapper` | `MAPPER_KAFKA_BOOTSTRAP_SERVERS`, `MAPPER_KAFKA_SECURITY_PROTOCOL`, `MAPPER_KAFKA_SASL_MECHANISM`, `MAPPER_KAFKA_USERNAME`, `MAPPER_KAFKA_PASSWORD` |
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу:
| Условие | STAND | Namespace | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `platform` | `0.0.1-stage` |
| ветка `master` | `preprod` | `mapper-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `mapper-prod` | `0.0.1-prod` |
| merge request | — (сборка образа отключена) | — | — |
Стадия `test`: job `linter` (`flake8 src/app`, `max-line-length=120`) и `typechecker` (`mypy src/app` с `types-redis`; `disallow_untyped_defs=True`).
## Замечания и потенциальные проблемы
- **`TIMEOUT` не читается приложением.** В Helm/Kustomize задаётся `TIMEOUT=120`, но клиенты читают `DOCUMENTATION_TIMEOUT`/`FLOW_TIMEOUT`/`DJANGO_TIMEOUT`/`NOTE_TIMEOUT` (каждый со своим префиксом). Без префикса переменная игнорируется — реальный таймаут остаётся `30`. Чтобы поднять таймаут, задавайте `<PREFIX>_TIMEOUT`.
- **Секреты Vault не используются кодом.** `MAPPER_DB_*`, `MAPPER_RABBITMQ_*`, `MAPPER_S3_*`, `MAPPER_KAFKA_*`, `MAPPER_DJANGO_TOKEN` монтируются и экспортируются в окружение (`base/deployment.yaml`), но текущая версия приложения ни PostgreSQL, ни RabbitMQ, ни S3, ни Kafka, ни `MAPPER_DJANGO_TOKEN` **не читает**`config.py` таких настроек нет). Похоже, инфраструктура заготовлена наперёд либо унаследована из шаблона.
- **Кеш отключён во всех окружениях (`REDIS_USE=0`).** При этом в `get_response` (`utils.py`) при не-200 ответе апстрима и выключенном кеше возвращается `None`, а роутер отдаёт `400 Bad Request`. То есть при выключенном Redis запасного кеша нет.
- **Подпись JWT не проверяется** (`verify_signature=False`) ни в режиме `zitadel`, ни в `sarex`. Доверие к токену — на сетевом слое (Istio/ingress). Публичный ключ не настраивается.
- **TLS-проверка апстримов отключена** (`httpx.AsyncClient(verify=False)`) для всех четырёх клиентов.
- **Нет healthcheck-эндпоинта.** Пробы `liveness`/`readiness` в чарте выключены — это согласовано.
- **Порты различаются:** локально `main.py` слушает `8002`, в контейнере gunicorn — `8000` (проброшен в k8s Service).
- **`docker-compose.yaml`** поднимает только Redis (redis-stack-server); само приложение в compose не описано.
## Минимальный набор для локального запуска
Поднять Redis (`docker compose up redis`) либо задать `REDIS_USE=False`, затем `uvicorn main:app` из `src`. Минимально стоит задать (у остальных есть рабочие дефолты для stage):
- `REDIS_USE` (`False`, если Redis не поднят) и при необходимости `REDIS_HOST`/`REDIS_PORT`
- при работе против нестандартных стендов — `DOCUMENTATION_HOST`, `FLOW_HOST`, `DJANGO_HOST`, `NOTE_HOST`
- `LOG_LEVEL` (по желанию)
Готовые значения-примеры приведены в `.env.example`.

95
apps/mapper/ENDPOINTS.md Normal file
View File

@ -0,0 +1,95 @@
# Эндпоинты сервиса mapper
Документ описывает HTTP-интерфейс сервиса `mapper` (flows mapper): собственные эндпоинты, которые сервис публикует, и внешние эндпоинты сервисов Sarex, к которым он обращается для сборки ответа.
## Как устроено взаимодействие
`mapper` — асинхронный FastAPI-прокси-агрегатор. Каждый входящий запрос:
1. проходит аутентификацию (`app/dependensies.py:get_user_data`) — из заголовков `Authorization` и опционального `Identity` извлекается `user_id` (подпись JWT не проверяется);
2. создаёт httpx-клиенты к нужным внешним сервисам (`app/config.py`, базовые хосты — из `*_HOST`, `verify=False`, заголовки авторизации пробрасываются);
3. параллельно-последовательно запрашивает 2 внешних сервиса через `ServiceManager.get_response()` (`app/utils.py`);
4. при `REDIS_USE=True` кеширует успешные (200) ответы в Redis по ключу `"{user_id}_{url}"`, а при ошибке апстрима возвращает данные из кеша;
5. объединяет ответы (`modify_pdm_data` / `modify_notes_data`) и отдаёт результат.
Если любой из двух апстримов вернул `None` (ошибка и нет кеша) — роутер отвечает `400 Bad Request`.
## Собственные эндпоинты (что публикует mapper)
Базовый префикс — `API_PREFIX` (по умолчанию `/api/v1`). Оба эндпоинта требуют заголовок `Authorization``Identity` для режима Zitadel).
| Метод | Путь | Назначение | Ответ |
| --- | --- | --- | --- |
| GET | `/api/v1/disks/{disk}/documents/` | Документы диска, обогащённые review-данными из сервиса процессов | `object` (`{"documents": [...]}`) |
| GET | `/api/v1/notes/{service}/{entity}/{instance_id}/` | Заметки сущности, обогащённые target-links из Django-бэкенда | `array` (список заметок) |
### `GET /api/v1/disks/{disk}/documents/`
Параметры пути: `disk` (string).
Логика (`routers.py:get_documents`):
- запрос к **documentations**: `GET /disks/{disk}/documents` → берётся поле `documents`;
- запрос к **flows**: `GET /documents/?full=true`;
- `modify_pdm_data` матчит по `document_id`/`bundle_id` и добавляет `review_data` в соответствующие бандлы документов.
### `GET /api/v1/notes/{service}/{entity}/{instance_id}/`
Параметры пути: `service`, `entity`, `instance_id` (string). Дополнительно **все query-параметры запроса пробрасываются** в сервис заметок (к ним добавляется `full=true`).
Логика (`routers.py:get_notes`):
- запрос к **notes**: `GET /notes/{service}/{entity}/{instance_id}/?full=true&<проброшенные query>`;
- запрос к **Django**: `GET /core/target-links/` (полный путь — `{DJANGO_HOST}/core/target-links/`);
- `modify_notes_data` заменяет id-ссылки в поле `links` каждой заметки на объекты target-links.
Полное описание схем — в `openapi.yaml`.
## Внешние сервисы и базовые хосты по окружениям
Базовые хосты берутся из `*_HOST` (`app/config.py`). Итоговый URL = `<HOST>` + путь ниже. Значения по окружениям — из `.helm/values.yaml` (деплой из репозитория сервиса) и overlay'ов инфра-репозитория.
| Сервис | Переменная | Дефолт в коде (stage) | preprod | production | brusnika-stage | brusnika-prod |
| --- | --- | --- | --- | --- | --- | --- |
| documentations | `DOCUMENTATION_HOST` | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.preprod.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | `https://test.sarex.brusnika.tech/documentations/api/v1` | `https://cde.brusnika.ru/documentations/api/v1` |
| flows | `FLOW_HOST` | `https://stage-api.sarex.io/flows/api/v1` | `https://api.preprod.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | `https://test.sarex.brusnika.tech/flows/api/v1` | `https://cde.brusnika.ru/flows/api/v1` |
| django (sarex-backend) | `DJANGO_HOST` | `https://stage.sarex.io/api` | `https://preprod.sarex.io/api` | `https://lk.sarex.io/api` | `https://test.sarex.brusnika.tech/api` | `https://cde.brusnika.ru/api` |
| notes | `NOTE_HOST` | `https://stage-api.sarex.io/notes/api/v1` | `https://api.preprod.sarex.io/notes/api/v1` | `https://api.sarex.io/notes/api/v1` | `https://test.sarex.brusnika.tech/notes/api/v1` | `https://cde.brusnika.ru/notes/api/v1` |
## Эндпоинты внешних сервисов (что вызывает mapper)
### `documentations`
| Метод | Путь | Параметры | Назначение | Где используется |
| --- | --- | --- | --- | --- |
| GET | `/disks/{disk}/documents` | `disk` (path) | Документы диска (поле `documents` в ответе) | `get_documents` |
### `flows`
| Метод | Путь | Параметры | Назначение | Где используется |
| --- | --- | --- | --- | --- |
| GET | `/documents/` | `full=true` (query) | Документы процессов с review-данными | `get_documents` |
### `notes`
| Метод | Путь | Параметры | Назначение | Где используется |
| --- | --- | --- | --- | --- |
| GET | `/notes/{service}/{entity}/{instance_id}/` | `service`, `entity`, `instance_id` (path); `full=true` + проброшенные query | Заметки сущности | `get_notes` |
### `django` (sarex-backend)
| Метод | Путь | Параметры | Назначение | Где используется |
| --- | --- | --- | --- | --- |
| GET | `/core/target-links/` | — | Связи (target-links); из ответа берётся `results`, если ответ — объект | `get_notes` |
## Заголовки и аутентификация
- `Authorization: <token>` — обязателен для обоих эндпоинтов; пробрасывается во все внешние запросы как есть.
- `Identity: <token>` — опционален; при наличии включается режим Zitadel, заголовок также пробрасывается во внешние запросы.
- Ответы при ошибках: `401 Unauthorized` (нет `Authorization`), `400 Bad Request` (ошибка апстрима без кеша), `422 Unprocessable Entity` (ошибка валидации параметров пути, стандартный ответ FastAPI).
## Замечания
- В коде клиент к сервису заметок называется `NoteSettings`/`note`, к Django — `DjangoSettings`/`django`. Пути `/documents/` (flows) и `/notes/.../` (notes) содержат завершающий слэш — важно для совпадения с маршрутами апстрима.
- Кеш ключуется по `user_id` + URL, поэтому проброшенные query-параметры в `get_notes` не входят в ключ кеша (URL берётся без query). При включённом Redis это стоит учитывать.
- Healthcheck-эндпоинта у сервиса нет.

230
apps/mapper/openapi.yaml Normal file
View File

@ -0,0 +1,230 @@
openapi: 3.0.3
info:
title: Mapper Service API
version: "1.0.0"
description: |
REST API сервиса **mapper** (flows mapper) — mini-HTTP-сервис, который
объединяет (мапит) данные нескольких сервисов Sarex: документы дисков из
сервиса документации (`documentations`) обогащаются review-данными из
сервиса процессов (`flows`); заметки из сервиса `notes` обогащаются
связями (target-links) из Django-бэкенда.
Сервис написан на Python (**FastAPI**), приложение создаётся в
`src/app/main.py` (`app = FastAPI()`), маршруты — в `src/app/routers.py`
с префиксом `API_PREFIX` (по умолчанию `/api/v1`).
### Аутентификация
Оба эндпоинта требуют заголовок `Authorization`. Опциональный заголовок
`Identity` включает режим Zitadel. Подпись JWT **не проверяется**
(`verify_signature=False`, `src/app/dependensies.py`) — доверие к токену
обеспечивается сетевым слоем (Istio/ingress). Из токена извлекается
`user_id`, который используется как часть ключа кеша Redis.
### Агрегация и кеш
Каждый запрос обращается к двум внешним сервисам через `ServiceManager`
(`src/app/utils.py`). При `REDIS_USE=True` успешные (200) ответы апстрима
кешируются в Redis (ключ `"{user_id}_{url}"`, TTL `REDIS_EXPIRE_DAYS`
суток), а при ошибке апстрима отдаётся кеш. Если хотя бы один апстрим
вернул ошибку и кеша нет — сервис отвечает `400 Bad Request`.
Схемы ответов заданы как свободные JSON-структуры (`object`/`array`),
так как сервис проксирует и объединяет ответы внешних сервисов без
фиксированной модели.
servers:
- url: https://api.sarex.io/mapper
description: production (Sarex)
- url: https://stage-api.sarex.io/mapper
description: stage (Sarex)
- url: https://cde.brusnika.ru/mapper
description: production (Brusnika)
- url: https://test.sarex.brusnika.tech/mapper
description: stage (Brusnika)
tags:
- name: documents
description: Документы дисков, обогащённые review-данными процессов
- name: notes
description: Заметки, обогащённые связями (target-links)
paths:
/api/v1/disks/{disk}/documents/:
get:
tags:
- documents
summary: Документы диска с review-данными
operationId: get_documents
description: |
Возвращает документы диска из сервиса документации, обогащённые
review-данными из сервиса процессов. Внутри выполняются запросы
`GET {DOCUMENTATION_HOST}/disks/{disk}/documents` и
`GET {FLOW_HOST}/documents/?full=true`, после чего review-данные
добавляются в поле `review_data` соответствующих бандлов документов
(`modify_pdm_data`).
security:
- bearerAuth: []
parameters:
- name: disk
in: path
required: true
description: Идентификатор диска
schema:
type: string
- name: Authorization
in: header
required: true
description: Токен доступа (пробрасывается во внешние сервисы)
schema:
type: string
- name: Identity
in: header
required: false
description: Identity-токен (включает режим Zitadel)
schema:
type: string
responses:
"200":
description: Успешный ответ
content:
application/json:
schema:
$ref: "#/components/schemas/DocumentsResponse"
"400":
description: Один из внешних сервисов недоступен и данных в кеше нет
"401":
description: Отсутствует заголовок Authorization
"422":
$ref: "#/components/responses/ValidationError"
/api/v1/notes/{service}/{entity}/{instance_id}/:
get:
tags:
- notes
summary: Заметки сущности со связями (target-links)
operationId: get_notes
description: |
Возвращает заметки сущности из сервиса `notes`, у которых поле `links`
заменено на объекты связей (target-links) из Django-бэкенда. Внутри
выполняются запросы
`GET {NOTE_HOST}/notes/{service}/{entity}/{instance_id}/?full=true`
(с проброской всех query-параметров запроса) и
`GET {DJANGO_HOST}/core/target-links/`, после чего выполняется
объединение (`modify_notes_data`).
security:
- bearerAuth: []
parameters:
- name: service
in: path
required: true
description: Логическое имя сервиса-владельца сущности
schema:
type: string
- name: entity
in: path
required: true
description: Тип сущности
schema:
type: string
- name: instance_id
in: path
required: true
description: Идентификатор экземпляра сущности
schema:
type: string
- name: Authorization
in: header
required: true
description: Токен доступа (пробрасывается во внешние сервисы)
schema:
type: string
- name: Identity
in: header
required: false
description: Identity-токен (включает режим Zitadel)
schema:
type: string
responses:
"200":
description: |
Успешный ответ — список заметок. Все дополнительные query-параметры
запроса проксируются в сервис заметок.
content:
application/json:
schema:
$ref: "#/components/schemas/NotesResponse"
"400":
description: Один из внешних сервисов недоступен и данных в кеше нет
"401":
description: Отсутствует заголовок Authorization
"422":
$ref: "#/components/responses/ValidationError"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
Токен передаётся заголовком `Authorization`. Подпись не проверяется
приложением. Для режима Zitadel дополнительно передаётся заголовок
`Identity`.
responses:
ValidationError:
description: Ошибка валидации параметров запроса
content:
application/json:
schema:
$ref: "#/components/schemas/HTTPValidationError"
schemas:
DocumentsResponse:
type: object
description: |
Ответ агрегатора документов. Структура повторяет ответ сервиса
документации, где в бандлы добавлено поле `review_data` с данными
из сервиса процессов.
properties:
documents:
type: array
items:
type: object
additionalProperties: true
additionalProperties: true
NotesResponse:
type: array
description: |
Список заметок сервиса `notes`, где поле `links` каждой заметки
заменено на объекты связей (target-links).
items:
type: object
additionalProperties: true
ValidationErrorItem:
type: object
properties:
loc:
type: array
items:
anyOf:
- type: string
- type: integer
msg:
type: string
type:
type: string
required:
- loc
- msg
- type
HTTPValidationError:
type: object
properties:
detail:
type: array
items:
$ref: "#/components/schemas/ValidationErrorItem"

View File

@ -0,0 +1,50 @@
# ============================================================================
# measurements — пример переменных окружения (HTTP-сервис на FastAPI)
#
# Скопируйте нужные строки в config.env / .env сервиса.
# Переменные, помеченные (обяз.), обязательны — без них процесс не стартует.
# Конфигурация читается через pydantic-settings (src/measurements/config.py).
# bool принимает 1/0, true/false, yes/no.
# ============================================================================
# --- S3 / MinIO (обяз.) -----------------------------------------------------
# Единственная обязательная переменная. JSON-строка с доступами к S3.
# Разбирается в S3CredentialsSettings.from_env(); если не задана —
# ValueError и процесс не стартует.
# Поля: host, login, password (обяз.), verify (bool, по умолч. false),
# buckets (список; если пуст — читается через list_buckets()).
S3_JSON_SETTINGS='{"host":"https://s3.example.com","login":"login","password":"password","verify":false,"buckets":["measurements"]}'
# --- Логирование (префикс LOG_) ---------------------------------------------
LOG_LEVEL=INFO # уровень логирования (по умолчанию INFO)
# LOG_FORMAT='{"timestamp": "%(asctime)s", "level": "%(levelname)s", "message": "%(message)s"}' # формат JSON-лога
# --- Приложение (ApplicationSettings, без префикса) -------------------------
# AUTH=0 # включить CustomAuthenticationMiddleware (по умолч. false)
# SHOW_UI=0 # показывать Swagger/redoc (по умолч. false — docs отключены)
# USE_SENTRY=0 # инициализировать Sentry (по умолч. false)
# DEBUG=0 # флаг отладки (по умолч. false)
# CLASSIC_MODE=1 # классический режим расчётов (по умолч. true)
# BLOCK_SIZE=256 # размер блока обработки растра (по умолч. 256)
# BLOCK_SIZE_FACTOR=10 # множитель площади блока (по умолч. 10)
# CPU_NUMBER=10 # число используемых CPU (по умолч. 10)
# --- Django / ЛК (префикс DJANGO_; читается при AUTH=1) ----------------------
# DJANGO_USE=1 # использовать интеграцию с Django (по умолч. true)
DJANGO_HOST=https://lk.sarex.io # базовый URL Django/ЛК (по умолч. https://lk.sarex.io)
# DJANGO_TIMEOUT=10 # таймаут HTTP-запросов к Django, сек (по умолч. 10)
# --- Sentry (префикс SENTRY_; читается при USE_SENTRY=1) ---------------------
# SENTRY_DSN= # DSN проекта Sentry (по умолч. пусто)
# SENTRY_ENVIRONMENT=production # окружение (по умолч. production)
# SENTRY_TRACES_SAMPLE_RATE=1.0 # доля трейсов (по умолч. 1.0)
# SENTRY_SEND_DEFAULT_PII=1 # отправлять PII (по умолч. true)
# --- Трейсинг OpenTelemetry (префикс TRACING_; читается при TRACING_USE=1) ---
TRACING_USE=0 # включить OTEL-трейсинг и otel-логгер (по умолч. false)
# TRACING_HOST=localhost:4317 # адрес OTLP-коллектора (по умолч. localhost:4317)
# TRACING_SERVICE_NAME=measurements # имя сервиса в трейсах (по умолч. measurements)
# TRACING_INSECURE=0 # подключение без TLS (по умолч. false)
# --- Задаётся в манифестах, кодом приложения НЕ читается ---------------------
# S3_JSON_FILE=/opt/cred_s3.json # присутствует в .helm/values.yaml, но код читает только S3_JSON_SETTINGS

View File

@ -0,0 +1,150 @@
# Конфигурация measurements
Документ описывает все переменные окружения и способы конфигурирования сервиса репозитория `measurements`:
- **measurements** — HTTP-сервис на FastAPI (`src/measurements`), запускается через gunicorn/uvicorn (`entrypoint.sh`, `measurements.main:app`). Считает измерения по растрам (GeoTIFF), читая их напрямую из S3/MinIO через GDAL (`vsis3`). Отдельного воркера у сервиса нет.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/measurements/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): классы `LoggerSettings`, `SentrySettings`, `DjangoSettings`, `ApplicationSettings`, `TraceSettings`, `S3CredentialsSettings`, `Store`. Отдельного конфиг-файла (yaml/toml) у приложения нет.
Каждый класс задаёт свой префикс через `class Config: env_prefix` (`LOG_`, `SENTRY_`, `DJANGO_`, `TRACING_`, `S3_`); у `ApplicationSettings` префикса нет — её поля читаются по имени напрямую (`AUTH`, `SHOW_UI`, `USE_SENTRY` и т.п.). Почти все переменные имеют значения по умолчанию, поэтому обязательна фактически одна — **`S3_JSON_SETTINGS`**: её отсутствие приводит к `ValueError` в `S3CredentialsSettings.from_env()` и процесс не стартует.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (docker-compose) | `docker-compose.yaml` — образ `measurements`, проброс порта `8000:8000`, инлайн `environment: S3_JSON_SETTINGS`. Сервис запускается `entrypoint.sh` (gunicorn, 4 воркера, uvicorn worker, таймаут 240) |
| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml` (universal-chart, dependency `oci://…/charts`): блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов). Per-env значения через ключи `_default`/`stage`/`preprod`/`production` |
| Kubernetes — этот infra-репозиторий (`iac/apps/measurements`) | `base/` — kustomize-манифесты с инъекцией секрета S3 через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + патч реплик), `brusnika-stage`/`brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блок `secretEnvs`) |
| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); в `workflow.rules` задаются переменные пайплайна (`STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `SERVICE_NAME`, `DOCKERFILE_PATH`) |
**Миграции БД.** Отсутствуют. Сервис не хранит собственное состояние в реляционной БД (`psycopg2` присутствует в зависимостях, но код измерений работает с растрами из S3). Шага миграций в `entrypoint.sh` нет.
---
## measurements (`measurements`)
Переменные читаются набором классов `*Settings` в `config.py`, инстанцируемых на уровне модуля: `settings = ApplicationSettings()`, `store = Store()`, `logger = LoggerSettings().logger`, `tracing_settings = TraceSettings()`.
### S3 / MinIO (обязательно)
Класс `S3CredentialsSettings`. Единственный обязательный источник конфигурации — переменная `S3_JSON_SETTINGS` (JSON-строка). Валидатор `from_env` (`model_validator(mode='before')`) читает её из окружения и при отсутствии выбрасывает `ValueError`. Доступы к S3 используются как boto3-клиентом (список бакетов), так и GDAL (`AWS_S3_ENDPOINT`/`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, драйвер `vsis3`).
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `S3_JSON_SETTINGS` | string (JSON) | да | — | JSON с доступами к S3. Поля: `host`, `login`, `password` (обяз.); `verify` (bool, по умолч. `false`); `buckets` (список; если пуст — бакеты запрашиваются через `list_buckets()`). Пример: `{"host":"https://s3…","login":"…","password":"…","verify":false,"buckets":["measurements"]}` |
> В `host` поддерживаются схемы `http://`/`https://`: при `http://` GDAL переключается на `AWS_HTTPS=NO`, отключает `GDAL_DISABLE_READDIR_ON_OPEN` и `AWS_VIRTUAL_HOSTING`.
### Логирование (префикс `LOG_`)
Класс `LoggerSettings`. Настраивает JSON-логгер (`python-json-logger`).
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `LOG_LEVEL` | string | нет | `INFO` | Уровень логирования (`INFO`/`DEBUG`/…); неизвестное значение → `INFO` |
| `LOG_FORMAT` | string | нет | JSON-шаблон | Формат строки лога для `JsonFormatter` |
### Приложение (`ApplicationSettings`, без префикса)
Поля читаются по имени напрямую (регистронезависимо). Управляют поведением сервиса и подключением middleware в `main.py`.
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `AUTH` | bool | нет | `false` | Подключить `CustomAuthenticationMiddleware` (проверка JWT `authorization`/`identity`) |
| `SHOW_UI` | bool | нет | `false` | Включить Swagger/redoc; при `false` `docs_url`/`redoc_url` отключены |
| `USE_SENTRY` | bool | нет | `false` | Инициализировать Sentry SDK и `SentryAsgiMiddleware` |
| `DEBUG` | bool | нет | `false` | Флаг отладки |
| `CLASSIC_MODE` | bool | нет | `true` | Классический режим расчётов |
| `BLOCK_SIZE` | int | нет | `256` | Размер блока обработки растра; участвует в `area_factor` |
| `BLOCK_SIZE_FACTOR` | int | нет | `10` | Множитель площади блока (`area_factor = BLOCK_SIZE_FACTOR × BLOCK_SIZE²`) |
| `CPU_NUMBER` | int | нет | `10` | Число используемых CPU |
> `DEBUG`, `CLASSIC_MODE`, `BLOCK_SIZE`, `BLOCK_SIZE_FACTOR`, `CPU_NUMBER` задаются в конфиге, но в текущих обработчиках напрямую не считываются (в `main.py` используются только `SHOW_UI`, `USE_SENTRY`, `AUTH`). Оставлены как настраиваемые параметры.
### Django / ЛК (префикс `DJANGO_`)
Класс `DjangoSettings`. Используется `CustomAuthenticationMiddleware`/`DjangoUserMiddleware` при включённой авторизации (`AUTH=1`) для запросов к ЛК.
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `DJANGO_USE` | bool | нет | `true` | Использовать интеграцию с Django |
| `DJANGO_HOST` | string | нет | `https://lk.sarex.io` | Базовый URL Django/ЛК |
| `DJANGO_TIMEOUT` | int | нет | `10` | Таймаут HTTP-запросов к Django, сек |
### Sentry (префикс `SENTRY_`)
Класс `SentrySettings`. Значения передаются в `sentry_sdk.init(**settings.sentry.kwargs)` только при `USE_SENTRY=1`.
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `SENTRY_DSN` | string | нет | `""` | DSN проекта Sentry |
| `SENTRY_ENVIRONMENT` | string | нет | `production` | Имя окружения в Sentry |
| `SENTRY_TRACES_SAMPLE_RATE` | float | нет | `1.0` | Доля трейсов |
| `SENTRY_SEND_DEFAULT_PII` | bool | нет | `true` | Отправлять PII |
### Трейсинг (OpenTelemetry, префикс `TRACING_`)
Класс `TraceSettings`. Активируется при `TRACING_USE=1` (`fastapi-otel-tools`).
| Переменная | Тип | Обяз. | По умолчанию | Назначение |
| --- | --- | --- | --- | --- |
| `TRACING_USE` | bool | нет | `false` | Включает OTEL-трейсинг, otel-логгер и `OtelMiddleware` |
| `TRACING_HOST` | string | нет | `localhost:4317` | Адрес OTLP-коллектора |
| `TRACING_SERVICE_NAME` | string | нет | `measurements` | Имя сервиса в трейсах |
| `TRACING_INSECURE` | bool | нет | `false` | Небезопасное (без TLS) подключение к коллектору |
> Тип `bool` в pydantic принимает `1`/`0`, `true`/`false`, `yes`/`no`.
---
## Инфраструктурные и вспомогательные переменные
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `S3_JSON_FILE` | `.helm/values.yaml` (`envs`) | Путь к файлу с доступами S3 (`/opt/cred_s3.json`). **Кодом не читается** — приложение использует только `S3_JSON_SETTINGS` |
| `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` (`variables`) | Параметры сборки образа |
| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `universal-pipeline` (деплой чарта per-env: `stage`/`preprod`/`production`) |
---
## Деплой из этого репозитория (`iac/apps/measurements`)
В `base/` используется **kustomize** (не собственный Helm-чарт сервиса). Доступ к S3/MinIO инъектируется агентом **Vault** и подгружается в окружение процесса до старта.
### `base/`
- `deployment.yaml` — единственный Deployment `measurements` (namespace `measurements`). Аннотации Vault (`agent-inject`, `role: measurements`) формируют шаблон секрета `measurements-s3` из `secrets/data/minio/apps/measurements`, собирая `S3_JSON_SETTINGS='{"host":…,"login":…,"password":…,"verify":false,"buckets":["measurements"]}'`. Контейнер запускается командой `set -a; . /vault/secrets/measurements-s3; set +a; exec /opt/entrypoint.sh`. Инлайн задан только `TRACING_USE=false`. Порт `8000` (`http`), `serviceAccountName: measurements-vault`, `imagePullSecrets: regcred`, ресурсы `cpu 25m` / `memory 128Mi`.
- `service.yaml``Service` `measurements-svc` (ClusterIP, порт `8000``8000`).
- `namespace.yaml` — namespace `measurements` с `istio-injection: enabled`.
- `serviceaccount.yaml` — SA `measurements-vault`.
- `kustomization.yaml` собирает `namespace`, `serviceaccount`, `deployment`, `service`.
### Оверлеи
- **`yc-k8s-test`** — `../base` + патч `replicas.yaml` (реплики Deployment `measurements` = 1).
- **`brusnika-stage`** / **`brusnika-prod`** — Flux `HelmRelease` `measurements` на `universal-chart` `0.1.7` (source `yc-oci-charts`). Секрет `S3_JSON_SETTINGS` берётся из k8s-секрета `s3-json-settings` (`secretEnvs`). `replicaCount`: `stage 1`, `preprod 3`, `production 3`; `imagePullSecrets: regcred`; `labels.monitoring: prometheus`; сервис `measurements-service` (ClusterIP, `8000`).
---
## Замечания и потенциальные проблемы
- **`S3_JSON_SETTINGS` — единственная жёстко обязательная переменная.** Локальный `docker-compose.yaml` задаёт её значением-заглушкой (`{"host":"host","login":"login","password":"password"}`) — для реальной работы значение нужно заменить.
- **`S3_JSON_FILE` в `.helm/values.yaml` кодом не читается** — приложение использует только `S3_JSON_SETTINGS` (в этом infra-репозитории она и инъектируется Vault). Расхождение способов передачи доступов между собственным чартом и infra-репо.
- **Опечатка в `middleware.py`:** в `DjangoUserMiddleware` используется `settings.django.self.timeout` вместо `settings.django.timeout` — лишний `.self` приведёт к `AttributeError`. Сам `DjangoUserMiddleware` в `main.py` не подключается (подключается `CustomAuthenticationMiddleware`).
- **Отсутствует поле `jwt_public_key`:** `CustomAuthenticationMiddleware` при отсутствии заголовка `identity` вызывает `jwt.decode(key=settings.jwt_public_key, …)`, но такого поля в `ApplicationSettings` нет — при `AUTH=1` и запросе без `identity` это приведёт к `AttributeError`. Если планируется проверка подписи, следует добавить переменную (напр. `JWT_PUBLIC_KEY`) в конфиг.
- **`brusnika-stage`/`brusnika-prod`: `image.name` указывает на `documentations` (`…/documentations:prod_5904312b`), а не на `measurements`** — вероятно скопировано из другого сервиса; для measurements образ должен указывать на `…/measurements`.
- **Проверки liveness/readiness отключены** во всех манифестах (`probes.*.enabled: false`); HTTP-эндпоинта healthcheck у сервиса нет.
- **Probes/порт в brusnika-оверлеях:** `deployment.port` задан `8080`, тогда как контейнер (`entrypoint.sh` → gunicorn) слушает `8000`, и `service.port`/`targetPort` = `8000`.
---
## Минимальный набор для локального запуска
- `S3_JSON_SETTINGS` (обязателен) — реальные доступы к S3/MinIO с бакетом(ами) растров.
- при необходимости: `LOG_LEVEL`, `TRACING_USE` (+ `TRACING_*`), `USE_SENTRY` (+ `SENTRY_*`), `AUTH` (+ `DJANGO_*`).
Сервис слушает `0.0.0.0:8000` (gunicorn, 4 воркера uvicorn). См. пример значений в `.env.example`.

View File

@ -0,0 +1,927 @@
{
"openapi": "3.1.0",
"info": {
"title": "measurements",
"description": "HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO через GDAL.\n\nВсе эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `<bucket>:<путь/к/файлу.tif>`; система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе координат растра.\n\nАутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.",
"version": "0.0.1"
},
"paths": {
"/api/point": {
"post": {
"summary": "Height/temperature at a single point",
"description": "Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3.",
"operationId": "point_api_point_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TiffPoint"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"type": "object",
"title": "Response Point Api Point Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/points": {
"post": {
"summary": "Height/temperature at multiple points",
"description": "Батч-версия /point: массив высот/температур для списка точек.",
"operationId": "points_api_points_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TiffPoints"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"additionalProperties": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"type": "object"
},
"type": "array",
"title": "Response Points Api Points Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/profile": {
"post": {
"summary": "Elevation profile along a polyline",
"description": "Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки.",
"operationId": "profile_api_profile_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TiffProfile"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"additionalProperties": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"type": "object"
},
"type": "array",
"title": "Response Profile Api Profile Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/volume": {
"post": {
"summary": "Volume inside a polygon",
"description": "Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек).",
"operationId": "volume_api_volume_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TiffVolume"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"additionalProperties": {
"type": "number"
},
"type": "object"
},
{
"type": "null"
}
],
"title": "Response Volume Api Volume Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/multiple": {
"post": {
"summary": "Volume difference between two DEMs",
"description": "Разница объёмов между master- и slave-растром в пределах полигона.",
"operationId": "multiple_api_multiple_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TiffMultiple"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "number"
},
"type": "object",
"title": "Response Multiple Api Multiple Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/transform": {
"post": {
"summary": "GeoTIFF affine transform",
"description": "Возвращает 6 коэффициентов GDAL GeoTransform растра.",
"operationId": "transform_api_transform_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Tiff"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 6,
"minItems": 6,
"title": "Response Transform Api Transform Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/info": {
"post": {
"summary": "GeoTIFF metadata",
"description": "Метаданные растра (размер, проекция, geotransform и т.п.).",
"operationId": "info_api_info_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Tiff"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"title": "Response Info Api Info Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/statistics": {
"post": {
"summary": "Raster min/max statistics",
"description": "Пара (min, max) значений растра.",
"operationId": "statistics_api_statistics_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Tiff"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2,
"title": "Response Statistics Api Statistics Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/bounds": {
"post": {
"summary": "Tile bounds by zoom range",
"description": "Границы тайлов (XYZ) для диапазона zoom_from..zoom_to.",
"operationId": "bounds_api_bounds_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Tiff"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"items": {
"items": {
"type": "integer"
},
"type": "array"
},
"type": "array"
},
"type": "object",
"title": "Response Bounds Api Bounds Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"Tiff": {
"properties": {
"path": {
"type": "string",
"title": "Path"
},
"zoom_to": {
"type": "integer",
"title": "Zoom To",
"default": 21
},
"zoom_from": {
"type": "integer",
"title": "Zoom From",
"default": 14
}
},
"type": "object",
"required": [
"path"
],
"title": "Tiff"
},
"TiffMultiple": {
"properties": {
"master_path": {
"type": "string",
"title": "Master Path"
},
"slave_path": {
"type": "string",
"title": "Slave Path"
},
"proj": {
"type": "string",
"title": "Proj",
"default": ""
},
"points": {
"items": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2
},
"type": "array",
"title": "Points"
}
},
"type": "object",
"required": [
"master_path",
"slave_path",
"points"
],
"title": "TiffMultiple",
"example": [
{
"master_path": "geotiff_dems/NTG030521_DEM.tif",
"points": [
[
37.34743572357015,
55.68881158675471
],
[
37.347128864435845,
55.6888704629146
],
[
37.34719182945381,
55.68896771656465
],
[
37.34732057806979,
55.688942063559054
]
],
"proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs",
"slave_path": "geotiff_dems/NTG030521_DEM.tif"
}
]
},
"TiffPoint": {
"properties": {
"altitude_path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Altitude Path"
},
"temperature_path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Temperature Path"
},
"proj": {
"type": "string",
"title": "Proj",
"default": ""
},
"point": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2,
"title": "Point"
}
},
"type": "object",
"required": [
"point"
],
"title": "TiffPoint",
"example": [
{
"altitude_path": "geotiff_dems/ALTITUDE_DEM.tif",
"point": [
59.95821631734607,
57.9660901731621
],
"proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs",
"temperature_path": "geotiff_dems/THERMAL_DEM.tif"
},
{
"point": [
1494214.348999979,
516505.3900003205
],
"proj": "",
"temperature_path": "geotiff_dems/THERMAL_DEM.tif"
},
{
"altitude_path": "geotiff_dems/ALTITUDE_DEM.tif",
"point": [
1494214.348999979,
516505.3900003205
],
"proj": ""
}
]
},
"TiffPoints": {
"properties": {
"altitude_path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Altitude Path"
},
"temperature_path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Temperature Path"
},
"proj": {
"type": "string",
"title": "Proj",
"default": ""
},
"points": {
"items": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2
},
"type": "array",
"title": "Points"
}
},
"type": "object",
"required": [
"points"
],
"title": "TiffPoints",
"example": [
{
"altitude_path": "geotiff_dems/ALTITUDE_DEM.tif",
"points": [
[
59.95821631734607,
57.9660901731621
],
[
59.95452603553467,
57.96567474229709
],
[
59.95563056285039,
57.966613723012216
]
],
"proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs",
"temperature_path": "geotiff_dems/THERMAL_DEM.tif"
},
{
"points": [
[
1494221.1549999786,
516495.86600015266
],
[
1494214.348999979,
516505.3900003205
]
],
"proj": "",
"temperature_path": "geotiff_dems/THERMAL_DEM.tif"
}
]
},
"TiffProfile": {
"properties": {
"path": {
"type": "string",
"title": "Path"
},
"proj": {
"type": "string",
"title": "Proj",
"default": ""
},
"points": {
"items": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2
},
"type": "array",
"title": "Points"
}
},
"type": "object",
"required": [
"path",
"points"
],
"title": "TiffProfile",
"example": [
{
"path": "geotiff_dems/NTG030521_DEM.tif",
"points": [
[
59.95821631734607,
57.9660901731621
],
[
59.95452603553467,
57.96567474229709
]
],
"proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs"
},
{
"path": "geotiff_dems/BLG_080122_DEM.tif",
"points": [
[
3334617.713961677,
590691.7743950449
],
[
3334977.6348458366,
590688.296080064
]
]
}
]
},
"TiffVolume": {
"properties": {
"path": {
"type": "string",
"title": "Path"
},
"proj": {
"type": "string",
"title": "Proj",
"default": ""
},
"level": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Level"
},
"points": {
"items": {
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
],
"type": "array",
"maxItems": 2,
"minItems": 2
},
"type": "array",
"title": "Points"
},
"mode": {
"allOf": [
{
"$ref": "#/components/schemas/VolumeMode"
}
],
"default": "cv2"
}
},
"type": "object",
"required": [
"path",
"points"
],
"title": "TiffVolume",
"example": [
{
"path": "geotiff_dems/NTG030521_DEM.tif",
"points": [
[
59.95821631734607,
57.9660901731621
],
[
59.95452603553467,
57.96567474229709
],
[
59.95563056285039,
57.966613723012216
]
],
"proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs"
},
{
"mode": "pillow",
"path": "geotiff_dems/NTG030521_DEM.tif",
"points": [
[
1494221.1549999786,
516495.86600015266
],
[
1494214.348999979,
516505.3900003205
]
],
"proj": ""
}
]
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"VolumeMode": {
"type": "string",
"enum": [
"cv2",
"pillow"
],
"title": "VolumeMode"
}
}
}
}

View File

@ -0,0 +1,565 @@
openapi: 3.1.0
info:
title: measurements
description: 'HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO
через GDAL.
Все эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `<bucket>:<путь/к/файлу.tif>`;
система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе
координат растра.
Аутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.'
version: 0.0.1
paths:
/api/point:
post:
summary: Height/temperature at a single point
description: Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3.
operationId: point_api_point_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TiffPoint'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
anyOf:
- type: number
- type: 'null'
type: object
title: Response Point Api Point Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/points:
post:
summary: Height/temperature at multiple points
description: 'Батч-версия /point: массив высот/температур для списка точек.'
operationId: points_api_points_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TiffPoints'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
items:
additionalProperties:
anyOf:
- type: number
- type: 'null'
type: object
type: array
title: Response Points Api Points Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/profile:
post:
summary: Elevation profile along a polyline
description: Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки.
operationId: profile_api_profile_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TiffProfile'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
items:
additionalProperties:
anyOf:
- type: number
- type: 'null'
type: object
type: array
title: Response Profile Api Profile Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/volume:
post:
summary: Volume inside a polygon
description: Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек).
operationId: volume_api_volume_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TiffVolume'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
anyOf:
- additionalProperties:
type: number
type: object
- type: 'null'
title: Response Volume Api Volume Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/multiple:
post:
summary: Volume difference between two DEMs
description: Разница объёмов между master- и slave-растром в пределах полигона.
operationId: multiple_api_multiple_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TiffMultiple'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: number
type: object
title: Response Multiple Api Multiple Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/transform:
post:
summary: GeoTIFF affine transform
description: Возвращает 6 коэффициентов GDAL GeoTransform растра.
operationId: transform_api_transform_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tiff'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
prefixItems:
- type: number
- type: number
- type: number
- type: number
- type: number
- type: number
type: array
maxItems: 6
minItems: 6
title: Response Transform Api Transform Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/info:
post:
summary: GeoTIFF metadata
description: Метаданные растра (размер, проекция, geotransform и т.п.).
operationId: info_api_info_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tiff'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
title: Response Info Api Info Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/statistics:
post:
summary: Raster min/max statistics
description: Пара (min, max) значений растра.
operationId: statistics_api_statistics_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tiff'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
title: Response Statistics Api Statistics Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/bounds:
post:
summary: Tile bounds by zoom range
description: Границы тайлов (XYZ) для диапазона zoom_from..zoom_to.
operationId: bounds_api_bounds_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Tiff'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
items:
items:
type: integer
type: array
type: array
type: object
title: Response Bounds Api Bounds Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
title: HTTPValidationError
Tiff:
properties:
path:
type: string
title: Path
zoom_to:
type: integer
title: Zoom To
default: 21
zoom_from:
type: integer
title: Zoom From
default: 14
type: object
required:
- path
title: Tiff
TiffMultiple:
properties:
master_path:
type: string
title: Master Path
slave_path:
type: string
title: Slave Path
proj:
type: string
title: Proj
default: ''
points:
items:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
type: array
title: Points
type: object
required:
- master_path
- slave_path
- points
title: TiffMultiple
example:
- master_path: geotiff_dems/NTG030521_DEM.tif
points:
- - 37.34743572357015
- 55.68881158675471
- - 37.347128864435845
- 55.6888704629146
- - 37.34719182945381
- 55.68896771656465
- - 37.34732057806979
- 55.688942063559054
proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22
+units=m +no_defs
slave_path: geotiff_dems/NTG030521_DEM.tif
TiffPoint:
properties:
altitude_path:
anyOf:
- type: string
- type: 'null'
title: Altitude Path
temperature_path:
anyOf:
- type: string
- type: 'null'
title: Temperature Path
proj:
type: string
title: Proj
default: ''
point:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
title: Point
type: object
required:
- point
title: TiffPoint
example:
- altitude_path: geotiff_dems/ALTITUDE_DEM.tif
point:
- 59.95821631734607
- 57.9660901731621
proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22
+units=m +no_defs
temperature_path: geotiff_dems/THERMAL_DEM.tif
- point:
- 1494214.348999979
- 516505.3900003205
proj: ''
temperature_path: geotiff_dems/THERMAL_DEM.tif
- altitude_path: geotiff_dems/ALTITUDE_DEM.tif
point:
- 1494214.348999979
- 516505.3900003205
proj: ''
TiffPoints:
properties:
altitude_path:
anyOf:
- type: string
- type: 'null'
title: Altitude Path
temperature_path:
anyOf:
- type: string
- type: 'null'
title: Temperature Path
proj:
type: string
title: Proj
default: ''
points:
items:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
type: array
title: Points
type: object
required:
- points
title: TiffPoints
example:
- altitude_path: geotiff_dems/ALTITUDE_DEM.tif
points:
- - 59.95821631734607
- 57.9660901731621
- - 59.95452603553467
- 57.96567474229709
- - 59.95563056285039
- 57.966613723012216
proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22
+units=m +no_defs
temperature_path: geotiff_dems/THERMAL_DEM.tif
- points:
- - 1494221.1549999786
- 516495.86600015266
- - 1494214.348999979
- 516505.3900003205
proj: ''
temperature_path: geotiff_dems/THERMAL_DEM.tif
TiffProfile:
properties:
path:
type: string
title: Path
proj:
type: string
title: Proj
default: ''
points:
items:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
type: array
title: Points
type: object
required:
- path
- points
title: TiffProfile
example:
- path: geotiff_dems/NTG030521_DEM.tif
points:
- - 59.95821631734607
- 57.9660901731621
- - 59.95452603553467
- 57.96567474229709
proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22
+units=m +no_defs
- path: geotiff_dems/BLG_080122_DEM.tif
points:
- - 3334617.713961677
- 590691.7743950449
- - 3334977.6348458366
- 590688.296080064
TiffVolume:
properties:
path:
type: string
title: Path
proj:
type: string
title: Proj
default: ''
level:
anyOf:
- type: number
- type: 'null'
title: Level
points:
items:
prefixItems:
- type: number
- type: number
type: array
maxItems: 2
minItems: 2
type: array
title: Points
mode:
allOf:
- $ref: '#/components/schemas/VolumeMode'
default: cv2
type: object
required:
- path
- points
title: TiffVolume
example:
- path: geotiff_dems/NTG030521_DEM.tif
points:
- - 59.95821631734607
- 57.9660901731621
- - 59.95452603553467
- 57.96567474229709
- - 59.95563056285039
- 57.966613723012216
proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22
+units=m +no_defs
- mode: pillow
path: geotiff_dems/NTG030521_DEM.tif
points:
- - 1494221.1549999786
- 516495.86600015266
- - 1494214.348999979
- 516505.3900003205
proj: ''
ValidationError:
properties:
loc:
items:
anyOf:
- type: string
- type: integer
type: array
title: Location
msg:
type: string
title: Message
type:
type: string
title: Error Type
type: object
required:
- loc
- msg
- type
title: ValidationError
VolumeMode:
type: string
enum:
- cv2
- pillow
title: VolumeMode

View File

@ -0,0 +1,87 @@
# =============================================================================
# Message Hub — пример конфигурации (.env)
# Версия: 0.1.0
# Скопируйте в .env и заполните значения.
# =============================================================================
# --- Приложение (префикс SETTINGS_) ---
SETTINGS_DEBUG=False
# Соответствие логических топиков реальным именам топиков Kafka.
# Допустимые ключи: planning, assets, issues
SETTINGS_TOPICS={"planning": "planning", "assets": "assets", "issues": "issues"}
# Проверять SSL-сертификаты у S3 и всех HTTP-клиентов внешних сервисов (1/0)
SETTINGS_VERIFY_SSL=1
SETTINGS_RETRY_DELAY=3
SETTINGS_MAX_RETRIES=3
SETTINGS_REQUEST_RETRIES=2
SETTINGS_REQUEST_DELAY=2
SETTINGS_MESSAGE_SKIP_AGE=300
SETTINGS_CACHE_EXPIRATION=120
SETTINGS_SENDER=noreply@sarex.io
# SETTINGS_WORKER_TIMEOUT=30
# --- Логирование (префикс LOG_) ---
# LOG_LEVEL=INFO
# LOG_FORMAT=[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s
# --- База данных PostgreSQL (префикс DB_) ---
DB_HOST=localhost
DB_PORT=5433
DB_DATABASE=sarex_db
DB_USERNAME=sarex
DB_PASSWORD=sarex
# DB_DIALECT=postgresql+psycopg
# --- Kafka (префикс KAFKA_) ---
KAFKA_HOST=
KAFKA_PORT=
KAFKA_USERNAME=
KAFKA_PASSWORD=
# PLAINTEXT | SSL | SASL_PLAINTEXT | SASL_SSL
KAFKA_SECURITY_PROTOCOL=
# PLAINTEXT | SCRAM-SHA-512
KAFKA_SASL_MECHANISM=
KAFKA_SSL_CAFILE=
# --- Redis / кеш (префикс CACHE_) ---
CACHE_HOST=localhost
CACHE_PORT=6378
CACHE_PASSWORD=
CACHE_SSL=0
# CACHE_SSL_CA_CERTS=/opt/ssl/ca.pem
# --- S3 (Yandex Object Storage, префикс S3_) ---
S3_HOST=http://localhost:9000
S3_LOGIN=minioadmin
S3_PASSWORD=minioadmin
S3_BUCKET=mybucket
# --- Внешние HTTP-сервисы (HOST / TIMEOUT на каждый префикс) ---
# Sarex backend
SAREX_HOST=http://localhost:8001
SAREX_TIMEOUT=60
# PM backend
PM_HOST=http://localhost:8001
PM_TIMEOUT=60
# Issues backend
ISSUES_HOST=http://localhost:8001
ISSUES_TIMEOUT=60
# BI backend
BI_HOST=http://localhost:8001
BI_TIMEOUT=60
# EAV service
EAV_HOST=http://localhost:8001
EAV_TIMEOUT=60
# HTML -> PDF converter (export-project)
PDF_CONVERTER_HOST=http://localhost:8001
PDF_CONVERTER_TIMEOUT=60
# Mailer service
MAILER_HOST=http://localhost:8001
MAILER_PREFIX=/api/v1
MAILER_TIMEOUT=60
# --- Инфраструктурные переменные (gunicorn / контейнер) ---
# Не читаются классом Settings, используются entrypoint.sh и docker-compose
# PYTHONPATH=src
# WORKERS=2
# WORKER_TIMEOUT=30

View File

@ -0,0 +1,208 @@
# Конфигурация проекта message-hub
# Версия: 0.1.0
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **через переменные окружения**. Разбор выполняется в `src/config/` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). Настройки разбиты на несколько классов, каждый со своим префиксом:
- `Settings` (`src/config/__init__.py`) — общий класс приложения, префикс `SETTINGS_`;
- `DBSettings` (`src/config/db.py`) — префикс `DB_`;
- `KafkaSettings` (`src/config/kafka.py`) — префикс `KAFKA_`;
- `RedisSettings` (`src/config/redis.py`) — префикс `CACHE_`;
- `ServiceConfig` и наследники (`src/config/sarex.py`) — префиксы `SAREX_`, `PM_`, `ISSUES_`, `BI_`, `EAV_`, `PDF_CONVERTER_`;
- `S3Settings` (`src/config/s3.py`) — префикс `S3_`;
- `MailerSettings` (`src/config/mailer.py`) — префикс `MAILER_`;
- `LoggerSettings` (`src/config/logger.py`) — префикс `LOG_`.
Особенности разбора:
- у каждого класса задан `env_file='.env'` и `extra='ignore'` — при наличии файла `.env` в рабочей директории он загружается автоматически, лишние переменные игнорируются;
- вложенных секций через разделитель нет — каждая группа настроек читается отдельным классом по своему префиксу;
- поле `VERIFY_SSL` в `S3Settings` и во всех `ServiceConfig` объявлено с `alias='SETTINGS_VERIFY_SSL'` — то есть единая переменная `SETTINGS_VERIFY_SSL` управляет проверкой TLS-сертификатов сразу для S3 и всех HTTP-клиентов внешних сервисов;
- у большинства полей есть значения по умолчанию, поэтому формально сервис стартует и без `.env`, но с дефолтными (локальными) адресами БД, Kafka, Redis и сервисов.
Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (бинарник) | Файл `.env` в рабочей директории (загружается `pydantic-settings`) и/или переменные окружения процесса |
| Локально (контейнеры) | `docker-compose.yaml`: блок `environment` для сервиса `message-hub` (`PYTHONPATH`, `KAFKA_HOST`, `KAFKA_PORT`) |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `universal-chart.services.message-hub.envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов); базовый чарт — `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и build-args |
Запуск процесса (`docker/entrypoint.sh`): единый ASGI-процесс поднимается через `gunicorn` с воркерами `uvicorn.workers.UvicornWorker` на `0.0.0.0:8000`:
```
gunicorn -w $WORKERS -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --timeout $WORKER_TIMEOUT --access-logfile - main:app
```
Приложение `main:app` (`src/main.py`) объединяет в одном ASGI-приложении: FastStream-брокер Kafka (потребители сообщений), HTTP-роуты health-проверок и Socket.IO-сервер (`AsyncServer` поверх `AsyncRedisManager`). Отдельных точек входа для воркеров/крон-задач нет — `pyproject.toml` не содержит `[project.scripts]`.
## Переменные приложения
### App (`SETTINGS_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SETTINGS_TOPICS` | dict (JSON) | `{}` | Соответствие логических топиков (`planning`/`assets`/`issues`) реальным именам топиков Kafka. Валидатор запрещает ключи вне набора `assets`/`planning`/`issues` |
| `SETTINGS_DEBUG` | bool | `False` | Режим отладки |
| `SETTINGS_VERIFY_SSL` | bool | `True` | Проверка TLS-сертификатов для S3 и всех HTTP-клиентов внешних сервисов (общий флаг через alias) |
| `SETTINGS_WORKER_TIMEOUT` | int | `30` | Таймаут воркера (поле `WORKER_TIMEOUT` класса `Settings`) |
| `SETTINGS_RETRY_DELAY` | int | `3` | Стартовая задержка (сек) между повторами обработки сообщения Kafka; удваивается на каждой попытке |
| `SETTINGS_MAX_RETRIES` | int | `3` | Число попыток обработки сообщения Kafka перед `ack` |
| `SETTINGS_REQUEST_RETRIES` | int | `2` | Число повторов HTTP-запросов к внешним сервисам |
| `SETTINGS_REQUEST_DELAY` | int | `2` | Задержка (сек) между повторами HTTP-запросов |
| `SETTINGS_MESSAGE_SKIP_AGE` | int | `300` | Возраст сообщения (сек), старше которого оно пропускается |
| `SETTINGS_CACHE_EXPIRATION` | int | `120` | TTL (сек) ключей присутствия пользователей в Redis (WebSocket) |
| `SETTINGS_SENDER` | string | `noreply@sarex.io` | Адрес отправителя по умолчанию |
### Логирование (`LOG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `INFO` | Уровень логирования (стандартные уровни `logging`) |
| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат строк лога |
### Database (`DB_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DB_HOST` | string | `''` | Хост PostgreSQL |
| `DB_PORT` | int | `5432` | Порт PostgreSQL |
| `DB_DATABASE` | string | `''` | Имя базы данных |
| `DB_USERNAME` | string | `''` | Пользователь БД |
| `DB_PASSWORD` | string | `''` | Пароль пользователя БД |
| `DB_DIALECT` | string | `postgresql+psycopg` | Диалект/драйвер SQLAlchemy. Итоговый DSN собирается в `db.url` |
### Kafka (`KAFKA_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_HOST` | string | `localhost` | Хост брокера |
| `KAFKA_PORT` | int | `9092` | Порт брокера |
| `KAFKA_USERNAME` | string \| null | `None` | Логин SASL |
| `KAFKA_PASSWORD` | string \| null | `None` | Пароль SASL |
| `KAFKA_SECURITY_PROTOCOL` | string | `PLAINTEXT` | Протокол безопасности: `PLAINTEXT`/`SSL`/`SASL_PLAINTEXT`/`SASL_SSL`. При `SSL`/`SASL_SSL` используется SSL-контекст |
| `KAFKA_SASL_MECHANISM` | string \| null | `None` | Механизм SASL. Обрабатываются `PLAINTEXT` и `SCRAM-SHA-512` |
| `KAFKA_SSL_CAFILE` | string \| null | `None` | Путь к CA-сертификату для SSL-контекста |
### Redis / кеш (`CACHE_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CACHE_HOST` | string | `localhost` | Хост Redis |
| `CACHE_PORT` | int | `6378` | Порт Redis |
| `CACHE_PASSWORD` | string \| null | `None` | Пароль Redis |
| `CACHE_SSL` | bool | `False` | Подключение по TLS (`rediss://`) |
| `CACHE_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату Redis |
> Redis используется как менеджер состояния Socket.IO (`AsyncRedisManager`) и как хранилище присутствия пользователей в проектах.
### S3 (`S3_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_HOST` | string | `https://storage.yandexcloud.net` | Endpoint S3 (`endpoint_url`) |
| `S3_LOGIN` | string | `''` | Access key |
| `S3_PASSWORD` | string | `''` | Secret key |
| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию |
| `SETTINGS_VERIFY_SSL` | bool | `True` | Проверять TLS-сертификат (общий флаг, см. App) |
### HTTP-клиенты внешних сервисов
Все клиенты наследуют общий класс `ServiceConfig` с полями `HOST`, `TIMEOUT` и общим флагом `VERIFY_SSL` (через alias `SETTINGS_VERIFY_SSL`). Значения по умолчанию: `HOST=http://localhost:8001`, `TIMEOUT=60`.
| Секция / префикс | Назначение |
| --- | --- |
| `SAREX_*` | Sarex backend (получение токенов клиентов и пр.) |
| `PM_*` | PM backend (синхронизация задач, автопланирование) |
| `ISSUES_*` | Сервис issues (типы задач, модели статусов) |
| `BI_*` | BI backend (синхронизация значений аналитики) |
| `EAV_*` | EAV-сервис (ассеты и атрибуты) |
| `PDF_CONVERTER_*` | Конвертер HTML → PDF (export-project) |
Для каждого — две переменные, напр. для PM:
| Переменная | Тип | Назначение |
| --- | --- | --- |
| `PM_HOST` | string | Базовый URL сервиса |
| `PM_TIMEOUT` | int | Таймаут запроса (сек) |
### Mailer (`MAILER_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `MAILER_HOST` | string | `http://localhost:8001` | Базовый URL сервиса рассылок |
| `MAILER_PREFIX` | string | `/api/v1` | Префикс маршрутов сервиса рассылок |
| `MAILER_TIMEOUT` | int | `60` | Таймаут запроса (сек) |
## Переменные инфраструктуры, сборки и запуска
Не читаются классами настроек приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `PYTHONPATH` | `docker-compose.yaml`, Helm `envs` | Каталог исходников (`src`) |
| `WORKERS` | `docker/entrypoint.sh`, Helm `envs` | Число воркеров gunicorn (по умолчанию `2`) |
| `WORKER_TIMEOUT` | `docker/entrypoint.sh`, Helm `envs` | Таймаут воркера gunicorn (`--timeout`) |
| `CI_COMMIT_SHORT_SHA` | `docker/Dockerfile` (build-arg через `BUILD_ARGS`) | Идентификатор сборки |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Сервис деплоится через зависимость `universal-chart`. Обычные значения задаются в блоке `universal-chart.services.message-hub.envs` для окружений `stage`/`preprod`/`production` (различаются адресами БД, Kafka, Redis, сервисов, именами топиков, числом реплик и таймаутами).
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`_default` / `production`) | Ключ (`secretKey`) |
| --- | --- | --- |
| `KAFKA_USERNAME` | `message-hub-kafka-secret` / `kafka-secret` | `username` |
| `KAFKA_PASSWORD` | `message-hub-kafka-secret` / `kafka-secret` | `password` |
| `DB_USERNAME` | `pm-postgresql-secret` / `postgres-pm-secret` | `user` |
| `DB_PASSWORD` | `pm-postgresql-secret` / `postgres-pm-secret` | `password` |
| `CACHE_PASSWORD` | `cache-secret-pm` / `cache-secret` | `password` |
| `S3_LOGIN` | `planning-s3-secret` / `s3-secret` | `username` |
| `S3_PASSWORD` | `planning-s3-secret` / `s3-secret` | `password` |
| `S3_BUCKET` | `planning-s3-secret` / `s3-secret` | `bucket` |
| `S3_HOST` | `planning-s3-secret` / `s3-secret` | `host` |
Помимо env, чарт монтирует CA-сертификат из секрета `kafka-secret` (ключ `ssl_cafile`) как файл `/opt/ssl/ca.pem` (том `kafka-ca-volume`, `readOnly`) — на него указывают `KAFKA_SSL_CAFILE` и `CACHE_SSL_CA_CERTS` в конфигурациях окружений.
Health-пробы (`.helm/values.yaml`): liveness `GET /health/live`, readiness `GET /health/ready`, порт `8000`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `planning` | `0.0.1-stage` |
| ветка `master` | `preprod` | `message-hub-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `message-hub-prod` | `0.0.1-prod` |
Ключевые переменные пайплайна: `SERVICE_NAME=message-hub`, `DOCKERFILE_PATH=./docker/Dockerfile`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE=app`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` (`--set universal-chart...`), флаг `ENABLE_BUILD_IMAGE`. Отдельные job'ы `linter` (`ruff check` / `ruff format --check`) и `typechecker` (`mypy src`).
## Замечания и потенциальные проблемы
- Единая переменная `SETTINGS_VERIFY_SSL` управляет проверкой TLS сразу для S3 и всех шести HTTP-клиентов (alias у поля `VERIFY_SSL`). Отдельно на клиент это не настраивается.
- Поле `WORKER_TIMEOUT` есть и в классе `Settings` (читается как `SETTINGS_WORKER_TIMEOUT`, дефолт `30`), и как самостоятельная переменная `WORKER_TIMEOUT` для gunicorn (`entrypoint.sh`, Helm). Это разные переменные — не перепутайте.
- `SETTINGS_TOPICS` валидируется: допустимы только ключи `assets`, `planning`, `issues`. Прочие ключи вызывают ошибку старта. Если ключ отсутствует, соответствующий потребитель подписывается на пустое имя топика.
- Файл `.env.example` в репозитории сервиса не содержит части переменных (сервисы `PM_/ISSUES_/BI_/EAV_/PDF_CONVERTER_`, `MAILER_`, `LOG_`, ряд `SETTINGS_*`) — при реальном запуске задавайте их явно (полный перечень — в данном документе и в `.env.example` рядом).
- У большинства полей есть дефолты (локальные адреса), поэтому при пустом окружении сервис поднимется, но будет ходить на `localhost` — для рабочих окружений значения задаются через Helm.
## Минимальный набор для локального запуска
Kafka поднимается через `docker-compose.yaml` (сервисы `kafka`, `kafka-ui`); PostgreSQL и Redis — внешние. Минимально стоит задать (с учётом префиксов):
- `SETTINGS_TOPICS` — карта логических топиков в реальные;
- `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`;
- `KAFKA_HOST`, `KAFKA_PORT` (для docker-compose — `kafka:9092`);
- `CACHE_HOST`, `CACHE_PORT` (+ `CACHE_PASSWORD`/`CACHE_SSL` при необходимости);
- `S3_HOST`, `S3_LOGIN`, `S3_PASSWORD`, `S3_BUCKET`;
- `HOST` для внешних сервисов, которые реально используются (`SAREX_`, `PM_`, `ISSUES_`, `BI_`, `EAV_`, `PDF_CONVERTER_`, `MAILER_`);
- `SETTINGS_VERIFY_SSL` (`0` локально, если сертификаты самоподписанные).
Готовые значения-примеры приведены в `.env.example` рядом с этим документом.

View File

@ -0,0 +1,94 @@
# Интерфейсы сервиса message-hub
# Версия: 0.1.0
Документ описывает интерфейсную поверхность сервиса: HTTP-эндпоинты, WebSocket (Socket.IO), потребляемые топики Kafka и исходящие HTTP-запросы к внешним сервисам.
> В отличие от классических backend-сервисов, у `message-hub` нет публичного REST API и, соответственно, нет OpenAPI-схемы (health-роуты объявлены с `include_in_schema=False`). Основные интерфейсы — это Kafka-потребители и Socket.IO. Поэтому файла `openapi.yaml` для сервиса нет.
## Как устроено взаимодействие
Единое ASGI-приложение (`main:app`, `src/main.py`) объединяет три поверхности:
- **HTTP** — health-проверки (`src/health.py`), обслуживаются FastStream ASGI;
- **WebSocket** — Socket.IO-сервер (`socketio.AsyncServer` + `AsyncRedisManager`), пространство имён `/project` (`src/ws/namespaces.py`);
- **Kafka** — потребители сообщений (`src/consumers/*.py`) на базе FastStream `KafkaRouter`.
Исходящие вызовы к внешним сервисам выполняются через `httpx.AsyncClient` (`src/config/sarex.py`, `mailer.py`), базовый хост берётся из соответствующего `*_HOST` (см. `CONFIGURATION.md`).
## HTTP-эндпоинты (входящие)
| Метод | Путь | Ответ | Назначение |
| --- | --- | --- | --- |
| GET | `/health/live` | `204 No Content` | Liveness-проба (всегда 204, если процесс жив) |
| GET | `/health/ready` | `204` / `500` | Readiness-проба: проверяет доступность Kafka (`broker.ping`) и Redis (`ping`); `500`, если хотя бы один недоступен |
Слушает `0.0.0.0:8000` (gunicorn + UvicornWorker). В Helm пробы настроены на `/health/live` и `/health/ready`, порт `8000`.
## WebSocket (Socket.IO)
Пространство имён: **`/project`** (`ProjectNamespace`). Менеджер состояния — Redis (`AsyncRedisManager`), CORS — `*`.
Параметры подключения (query string при `connect`): `project_id` (int), `user_id` (int). Клиент помещается в комнату `project:{project_id}`.
| Направление | Событие | Данные | Назначение |
| --- | --- | --- | --- |
| client → server | `connect` | query: `project_id`, `user_id` | Подключение; вход в комнату проекта, регистрация присутствия в Redis |
| client → server | `heartbeat` | — | Продление TTL присутствия пользователя в проекте |
| client → server | `disconnect` | — | Отключение; выход из комнаты, снятие присутствия |
| server → client | `connected_users` | `list[int]` (user_id) | Актуальный список пользователей, подключённых к проекту (рассылается в комнату `project:{project_id}` при connect/disconnect) |
Ключи присутствия в Redis (TTL = `SETTINGS_CACHE_EXPIRATION`): `project:{project_id}:{user_id}`, `user_sids:{project_id}:{user_id}:{sid}`.
## Kafka-потребители (входящие сообщения)
Реальные имена топиков задаются переменной `SETTINGS_TOPICS` (маппинг логических имён `planning`/`assets`/`issues` в имена топиков). Формат сообщения — `MessageSchema` (`src/schemas/message.py`): поля `schema_version`, `model`, `sender`, `type`, `body`, `timestamp`, `xtraceId`, `user_id`, `tenants`, `tags`.
| Топик (логич.) | `group_id` | Offset reset | Обработчик | Назначение |
| --- | --- | --- | --- | --- |
| `assets` | `assets_consumer` | earliest | `update_attributes_with_assets` | Обновление атрибутов по ассетам |
| `planning` | `planning` | earliest | диспетчер по `type` (см. ниже) | Обработка событий планирования |
| `issues` | `project_entity` | earliest | `create_or_update_entity` | Создание/обновление сущности проекта из issue |
| `issues` | `analytic_values` | latest | `proceed_entity_value` | Обработка значений аналитики по сущности |
Диспетчеризация топика `planning` по полю `type` (`src/consumers/planning.py`):
| `type` сообщения | Обработчик | Назначение |
| --- | --- | --- |
| `auto_scheduling` | `handle_auto_scheduling` | Автопланирование |
| `get_converted_file` | `handle_file_export` | Экспорт/конвертация файла |
| `system_log` | `handle_system_log` | Системный журнал изменений |
| `email_notifications` | `handle_email_notification` | Email-уведомления |
| `sync_entity_to_project` | `handle_project_entity_sync` | Синхронизация сущности в проект |
| `sync_detailed_tasks_attributes` | `handle_task_attributes_sync` | Синхронизация атрибутов детальных задач |
| `sync_tasks` | `handle_task_sync` | Синхронизация задач |
| `detailed_tasks_analytics` | `handle_task_analytics` | Аналитика по детальным задачам |
| `update_project` | — | Пропускается (в списке `PLANNING_SKIP_TYPES`) |
Обработка обёрнута в `retry_handler` (`src/infrastructure/kafka/retry.py`): до `SETTINGS_MAX_RETRIES` попыток с экспоненциальной задержкой (старт `SETTINGS_RETRY_DELAY`), ручной `ack` после успеха либо исчерпания попыток.
## Исходящие HTTP-запросы к внешним сервисам
Базовый хост каждого сервиса — из соответствующего `*_HOST` (см. `CONFIGURATION.md`). Итоговый URL = `<HOST>` + путь из таблицы.
| Сервис (`config`) | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `bi` (`BI_HOST`) | POST | `/internal/values/sync_value/` | Синхронизация значений аналитики |
| `pm` (`PM_HOST`) | POST | `/internal/pm/detailed_tasks/` | Синхронизация атрибутов детальных задач |
| `pm` (`PM_HOST`) | POST | `/internal/pm/{endpoint}/` | Автопланирование (endpoint из тела сообщения) |
| `pm` (`PM_HOST`) | POST | `/internal/pm/sync_tasks/` | Синхронизация задач |
| `pdf` (`PDF_CONVERTER_HOST`) | POST | `/convert_to_pdf/` | Конвертация HTML → PDF |
| `mailer` (`MAILER_HOST`) | POST | `{MAILER_PREFIX}/emails/bulk` | Массовая отправка email |
| `issues` (`ISSUES_HOST`) | GET | `/api/issue-types/?company_id={id}` | Список типов issue компании |
| `issues` (`ISSUES_HOST`) | GET | `/api/companies/{company_id}/status-model/v2/?issue_type_id={id}` | Модель статусов по типу issue |
| `eav` (`EAV_HOST`) | POST | `/api/v4/assets/search/` | Поиск ассетов по идентификаторам |
| `eav` (`EAV_HOST`) | GET | `/api/v4/attribute/` | Список атрибутов |
| `sarex` (`SAREX_HOST`) | GET | `internal/client/token/{user_id}/` | Получение токена клиента |
## Внешние зависимости (инфраструктура)
| Зависимость | Назначение |
| --- | --- |
| Kafka | Источник сообщений (топики `planning`/`assets`/`issues`) |
| PostgreSQL | Хранилище данных (SQLAlchemy + psycopg) |
| Redis | Менеджер состояния Socket.IO и хранилище присутствия пользователей |
| S3 (Yandex Object Storage) | Файловое хранилище |

56
apps/notes/.env.example Normal file
View File

@ -0,0 +1,56 @@
# App
BASE_HOST=https://stage-api.sarex.io/notes
API_PREFIX=/api/v1
# DEBUG влияет и на PostgresSettings (при true хост БД -> localhost:6432), и на Settings.debug
DEBUG=false
REGISTRY=cr.yandex/crp3ccidau046kdj8g9q/
LOG_LEVEL=INFO
# Database (префикс PG_)
PG_LOGIN=notes
PG_PASSWORD=notes
PG_DB=notes_db
PG_HOST=127.0.0.1
PG_PORT=5432
# В коде подключение к БД всегда идёт с sslmode=verify-full (см. замечания в CONFIGURATION.md)
PG_SSL_MODE=verify-full
# Django (sarex-backend, префикс DJANGO_)
# DJANGO_USE=false — отключает проверку токена через Django (локальная разработка, тестовый пользователь)
DJANGO_USE=false
DJANGO_HOST=https://stage.sarex.io
DJANGO_TIMEOUT=10
DJANGO_TOKEN=token
# Documentations (префикс DOCUMENTATIONS_)
DOCUMENTATIONS_HOST=https://stage-api.sarex.io/documentations/api/v1
# Workflows (префикс WORKFLOW_)
WORKFLOW_HOST=https://stage-api.sarex.io/workflows/api/v1
WORKFLOW_TAG=dev
WORKFLOW_TIMEOUT=30
# Attachments (префикс ATTACHMENT_)
ATTACHMENT_HOST=http://attachments-service.attachments-stage.svc.cluster.local:80/api/v1
ATTACHMENT_TIMEOUT=30
# Внешние сервисы (top-level Settings)
FAAS_SERVICE=https://stage-api.sarex.io/lambdas
WORKSPACE_URL=https://stage-api.sarex.io/workspaces/api/v1
RESOURCE_URL=https://stage-api.sarex.io/resources/api/v1
# Включает вычисление resource_id по workspace/target при создании заметки
SYNC_RESOURCE_ID=false
# ND-сервис (проксирование НД, префиксов нет)
ENABLE_ND=false
ND_JWT_ENABLE=false
ND_JWT_SECRET=
ND_JWT_ALGORITHM=HS256
ND_ACCESS_TOKEN_EXPIRE_DAYS=30
# Logger (префикс LOG_)
# LOG_FORMAT='[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s'
# Инфраструктура / запуск (не читаются кодом приложения)
# Таймаут воркеров gunicorn (entrypoint.sh)
TIMEOUT=120

184
apps/notes/CONFIGURATION.md Normal file
View File

@ -0,0 +1,184 @@
# Конфигурация проекта notes-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/app/config.py` через библиотеку [`pydantic`](https://docs.pydantic.dev/) (`pydantic.BaseSettings`, pydantic v1).
Конфигурация разбита на несколько классов настроек, каждый со своим префиксом (`Config.env_prefix`):
- `PostgresSettings` — префикс `PG_`;
- `DjangoSettings` — префикс `DJANGO_`;
- `Documentations` — префикс `DOCUMENTATIONS_`;
- `WorkflowSettings` — префикс `WORKFLOW_`;
- `AttachmentSettings` — префикс `ATTACHMENT_`;
- `LoggerSettings` — префикс `LOG_`;
- корневой `Settings`**без префикса** (поля читаются по имени в верхнем регистре, напр. `BASE_HOST`, `FAAS_SERVICE`).
Каждый вложенный класс настроек инстанцируется отдельно и читает свои переменные из окружения по своему префиксу. Отдельного конфиг-файла (yaml/toml) у приложения нет.
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (uvicorn/gunicorn) | Переменные окружения процесса. Приложение **не загружает `.env` автоматически**`config.py` нет `env_file`/`python-dotenv`) — переменные нужно экспортировать самому |
| Локально (контейнеры) | `docker-compose.yml`: блок `environment` для сервиса `notes` (`PG_HOST`, `PG_DB`, `PG_LOGIN`, `PG_PASSWORD`, `DJANGO_USE`, `TIMEOUT`) |
| Kubernetes (Helm) | `.helm/values.yaml`: блоки `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) чарта `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) — выбор окружения по ветке/тегу |
Порядок запуска в контейнере (`entrypoint.sh`): сначала выполняются миграции (`alembic upgrade head`), затем стартует gunicorn с воркерами `uvicorn.workers.UvicornWorker` на `0.0.0.0:8000` с таймаутом `$TIMEOUT`.
## Переменные приложения
Дефолт `—` означает, что явного значения по умолчанию нет (пустая строка/обязательно задать для реального окружения).
### App / корневой `Settings` (без префикса)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `BASE_HOST` | string | `https://lk.sarex.io` | Внешний базовый URL сервиса |
| `API_PREFIX` | string | `/api/v1` | Префикс публичного API |
| `DEBUG` | bool | `False` | Режим отладки. Также влияет на `PostgresSettings` (см. ниже) и включает `ProfilingSqlQueryMiddleware` |
| `FAAS_SERVICE` | string | `https://stage-api.sarex.io/lambdas` | URL сервиса лямбд/FaaS |
| `WORKSPACE_URL` | string | `https://stage-api.sarex.io/workspaces/api/v1` | URL сервиса рабочих областей (для `SYNC_RESOURCE_ID`) |
| `RESOURCE_URL` | string | `https://stage-api.sarex.io/resources/api/v1` | URL сервиса ресурсов (для `SYNC_RESOURCE_ID`) |
| `SYNC_RESOURCE_ID` | bool | `False` | При `True` `resource_id` заметки вычисляется по workspace → target → resource |
| `REGISTRY` | string | `cr.yandex/crp3ccidau046kdj8g9q/` | Реестр образов (используется вспомогательно) |
| `ENABLE_ND` | bool | `False` | Подключить роутер `nd_service` (`/api/v1/nd/*`) |
### ND-сервис (без префикса)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ND_JWT_ENABLE` | bool | `False` | Включить проверку JWT (`JWTBearer`) на части эндпоинтов НД |
| `ND_JWT_SECRET` | string | `""` | Секрет для подписи/проверки JWT |
| `ND_JWT_ALGORITHM` | string | `HS256` | Алгоритм JWT |
| `ND_ACCESS_TOKEN_EXPIRE_DAYS` | int | `30` | Срок жизни токена НД (дни) |
### Database (`PG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `PG_LOGIN` | string | `""` | Пользователь PostgreSQL |
| `PG_PASSWORD` | string | `""` | Пароль пользователя |
| `PG_DB` | string | `""` | Имя базы данных |
| `PG_HOST` | string | `""` | Хост PostgreSQL |
| `PG_PORT` | string | `5432` | Порт PostgreSQL |
| `PG_SSL_MODE` | string | `disable` | Поле `ssl_mode` настроек (см. замечание ниже — фактически подключение всегда `verify-full`) |
| `DEBUG` | bool | `False` | Через `Field(env='DEBUG')`. При `True` хост БД принудительно `localhost:6432` (pgbouncer) |
> Итоговый DSN собирается в `PostgresSettings.url` как `postgresql://<login>:<password>@<host>:<port>/<db>`.
### Django / sarex-backend (`DJANGO_*`)
Клиент к основному backend (Django). Используется middleware `DjangoUserMiddleware` для аутентификации пользователя (запрос `/client/settings/`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DJANGO_USE` | bool | `True` | При `False` аутентификация через Django отключается, используется тестовый пользователь (`is_admin=True`) |
| `DJANGO_HOST` | string | `http://localhost:8000` | Базовый хост Django (к нему добавляется `/api`) |
| `DJANGO_TIMEOUT` | int | `10` | Таймаут HTTP-клиента (сек) |
| `DJANGO_TOKEN` | string | `token` | Токен для служебных (sync) запросов |
### Documentations (`DOCUMENTATIONS_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DOCUMENTATIONS_HOST` | string | `https://stage-api.sarex.io/documentations/api/v1` | URL сервиса документации |
### Workflows (`WORKFLOW_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `WORKFLOW_HOST` | string | `https://stage-api.sarex.io/workflows/api/v1` | URL сервиса обработки процессов |
| `WORKFLOW_TAG` | string | `dev` | Тег/канал workflow (`dev`/`stable`) |
| `WORKFLOW_TIMEOUT` | int | `30` | Таймаут HTTP-клиента (сек) |
### Attachments (`ATTACHMENT_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ATTACHMENT_HOST` | string | `http://attachments-service.attachments-stage.svc.cluster.local:80/api/v1` | URL сервиса вложений |
| `ATTACHMENT_TIMEOUT` | int | `30` | Таймаут HTTP-клиента (сек) |
### Logger (`LOG_*`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `INFO` | Уровень логирования (`DEBUG`/`INFO`/…); при неизвестном значении используется `INFO` |
| `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат сообщений лога |
## Переменные инфраструктуры, сборки и вспомогательных утилит
Не читаются кодом приложения, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `TIMEOUT` | `docker-compose.yml`, `.helm/values.yaml`, `entrypoint.sh` | Таймаут воркеров gunicorn (`--timeout`) |
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `docker-compose.yml` (сервис `database`) | Параметры локального контейнера Postgres |
| `PGADMIN_DEFAULT_EMAIL` / `PGADMIN_DEFAULT_PASSWORD` | `docker-compose.yml` (сервис `pgadmin`) | Учётные данные pgAdmin для локальной разработки |
| `NPM_NEXUS_TOKEN` | (для фронтенда) | Токен приватного npm-реестра — здесь не используется |
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт — `universal-chart` (зависимость в `.helm/Chart.yaml`). Обычные значения задаются в блоке `services.main.envs` и различаются по окружениям (`_default`/`stage`/`preprod`/`production`):
| Переменная | `_default` (stage) | `preprod` | `production` |
| --- | --- | --- | --- |
| `PG_SSL_MODE` | `verify-full` | — | — |
| `PG_PORT` | `6432` | — | — |
| `DJANGO_HOST` | `https://stage.sarex.io` | `https://lk.preprod.sarex.io` | `https://lk.sarex.io` |
| `BASE_HOST` | `https://stage-api.sarex.io/notes` | `https://api.preprod.sarex.io/notes` | `https://api.sarex.io/notes` |
| `TIMEOUT` | `120` | — | — |
| `FAAS_SERVICE` | `https://stage-api.sarex.io/lambdas` | `https://api.preprod.sarex.io/lambdas` | `https://api.sarex.io/lambdas` |
| `WORKSPACE_URL` | `https://stage-api.sarex.io/workspaces/api/v1` | `https://api.preprod.sarex.io/workspaces/api/v1` | `https://api.sarex.io/workspaces/api/v1` |
| `WORKFLOW_HOST` | `https://stage-api.sarex.io/workflows/api/v1` | `https://api.preprod.sarex.io/workflows/api/v1` | `https://api.sarex.io/workflows/api/v1` |
| `WORKFLOW_TAG` | `dev` | `stable` | `stable` |
| `RESOURCE_URL` | `https://stage-api.sarex.io/resources/api/v1` | `https://api.preprod.sarex.io/resources/api/v1` | `https://api.sarex.io/resources/api/v1` |
| `SYNC_RESOURCE_ID` | `0` | — | — |
| `ENABLE_ND` | `1` | `0` | `0` |
| `ATTACHMENT_HOST` | `…attachments-stage…` | `…attachments-preprod…` | `…attachments-prod…` |
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Переменная | Секрет (`secretName`, stage) | Ключ (`secretKey`) |
| --- | --- | --- |
| `PG_DB` | `notes-postgresql-secret` | `database` |
| `PG_LOGIN` | `notes-postgresql-secret` | `username` |
| `PG_PASSWORD` | `notes-postgresql-secret` | `password` |
| `PG_HOST` | `notes-postgresql-secret` | `host` |
| `DJANGO_TOKEN` | `django-secret` | `token` |
Прочие значения чарта (не переменные приложения): `deployment.*` (имя, порт `8000`, реплики, ресурсы, probes отключены), `image.*`, `service.*` (`notes-backend-service`, в production — `backend-service`), `imagePullSecrets` (`dockerhub`), `ingress.enabled: false`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `aero` | `0.0.1-stage` |
| ветка `master` | `preprod` | `notes-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `notes-prod` | `0.0.1-prod` |
Ключевые переменные: `SERVICE_NAME=notes-backend`, `DOCKERFILE_PATH`, `RELEASE_NAME`, `CHART_NAME`, `HELM_SET_ARGS` (проброс образа/окружения в `universal-chart`). Джобы `linter` (flake8), `typechecker` (mypy), `rest-api` (docker-compose + newman/postman) выполняются на MR/ветках/тегах.
## Замечания и потенциальные проблемы
- **SSL к БД всегда `verify-full`.** Поле `PG_SSL_MODE` (по умолчанию `disable`) в код подключения не попадает: и `PostgresSettings.create_session`, и `DBSessionMiddleware` жёстко передают `connect_args={'sslmode': "verify-full"}`. CA-сертификат монтируется из образа: `Dockerfile` копирует `yandex_pg.pem``/root/.postgresql/root.crt`.
- **`DEBUG` — общая переменная.** Она читается и корневым `Settings.debug`, и `PostgresSettings.debug` (`Field(env='DEBUG')`). При `DEBUG=true` хост БД принудительно становится `localhost:6432`, а также включается `ProfilingSqlQueryMiddleware`.
- **Приложение не загружает `.env` автоматически** — переменные нужно экспортировать в окружение (или задавать через `--env`/compose/helm).
- **Аутентификация.** При `DJANGO_USE=true` каждый публичный запрос (кроме путей с `/nd`) проверяется через Django `/client/settings/` по заголовку `Authorization` (опционально `Identity` для Zitadel). При `DJANGO_USE=false` подставляется тестовый администратор — использовать только локально.
- **Роутер НД включается флагом `ENABLE_ND`.** На stage он включён (`1`), на preprod/production выключен (`0`).
- Значение `SYNC_RESOURCE_ID` требует доступности `WORKSPACE_URL` и `RESOURCE_URL`; клиент к ним создаётся с `verify=False`.
## Минимальный набор для локального запуска
Postgres и pgAdmin поднимаются через `docker-compose up -d database pgadmin`. Минимально необходимо задать:
- `PG_LOGIN`, `PG_PASSWORD`, `PG_DB`, `PG_HOST`, `PG_PORT`
- `DJANGO_USE=false` (чтобы не требовать реальный Django-токен)
- при `ENABLE_ND=true``ND_JWT_*` при необходимости проверки токена
Остальные значения имеют рабочие дефолты (см. `.env.example`).

76
apps/notes/ENDPOINTS.md Normal file
View File

@ -0,0 +1,76 @@
# Эндпоинты, с которыми взаимодействует notes-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `notes-frontend`, remote-имя `srx_notes`).
## Как устроено взаимодействие
Все запросы собраны в объекте `notesApi` в `module/api/endpoints.ts`. Каждый метод вызывает `httpService` (`module/api/http-service.ts`, обёртка над `@sarex-team/sdk-js`) одним из методов `getRequest` / `postRequest` / `putRequest` / `deleteRequest`, передавая:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (относительно базового хоста сервиса);
- `data` — тело запроса (для POST/PUT).
Базовый хост подставляется `httpService` из `module/api/hosts.ts` в зависимости от `BUILD_ENV` (`local`/`stage`/`preprod`/`prod`). `BUILD_ENV` задаётся через webpack `DefinePlugin` на этапе сборки (`build.config.js`). Итоговый URL = `<базовый хост сервиса>` + `url`. Удалённый модуль `documentations` (Module Federation) подключается отдельно через `module/api/modules-hosts.ts`.
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`.
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `notes` | Бэкенд заметок (notes-backend) | `https://stage-api.sarex.io/notes` | `https://api.sarex.io/notes` |
| `sarexApi` | Gateway/API Sarex (`/eav`, `/notes`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `documentations` | Сервис документации (бандлы) | `https://stage-api.sarex.io/documentations/` | `https://api.sarex.io/documentations/` |
| `sarex` | Основной backend Sarex (`/api/core`) | `""` (относительные пути) | `""` |
| `workspaces` | Сервис рабочих областей | `https://stage-workspaces.sarex.io` | `https://workspaces.sarex.io` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определены окружения `local` и `preprod`. В `local` `sarex` указывает на `https://stage.sarex.io`, в остальных — пустая строка (относительные пути). Сервисы `workspaces` и `zitadel` объявлены в хостах, но напрямую из `endpoints.ts` не вызываются. Удалённый модуль `documentations` описан в `module/api/modules-hosts.ts` (`…/documentations/static/module/remoteEntry.js`).
## Эндпоинты по сервисам
### `notes` — Бэкенд заметок (notes-backend)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `createNote` | POST | `/api/v1/notes/` | Создать заметку |
| `getNote` | GET | `/api/v1/notes/{id}/` | Заметка по id |
| `updateNote` | PUT | `/api/v1/notes/{id}/` | Обновить заметку |
| `deleteNote` | DELETE | `/api/v1/notes/{id}/` | Удалить заметку |
| `getNoteAttachments` | GET | `/api/v1/notes/{noteId}/attachments/` | Вложения заметки |
| `createAttachmentsToNote` | POST | `/api/v1/notes/{noteId}/attachments/` | Загрузить вложения к заметке |
| `deleteAttachmentsFromNote` | DELETE | `/api/v1/attachments/{attachmentId}/` | Удалить вложение |
| `generateDocument` | POST | `/api/v1/notes/{noteId}/generate_document/` | Сгенерировать документ по заметке |
| `postScreen` | POST | `/api/v1/nd/bound-note/{noteId}/` | Привязать скриншот/файл к заметке (НД) |
### `sarexApi` — Gateway/API Sarex
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getAttributes` | GET | `/eav/api/v0/attribute/` | Атрибуты (EAV) |
| `createLinkNote` | POST | `/notes/api/v1/links/` | Привязать ссылку к заметке (через gateway) |
### `documentations` — Сервис документации
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getBundle` | GET | `/api/v1/bundles/{id}` | Бандл по id |
### `sarex` — Основной backend Sarex (`/api/core`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getCompanies` | GET | `/api/core/companies/` | Список компаний |
| `createLink` | POST | `/api/core/target-links/` | Создать ссылку у target |
| `updateLink` | PUT | `/api/core/target-links/{id}/` | Обновить ссылку |
| `deleteLink` | DELETE | `/api/core/target-links/{id}/` | Удалить ссылку |
## Обработка ошибок
Централизованного модуля обработки ошибок (аналога `errors.ts`) нет. Ответы `httpService` (`@sarex-team/sdk-js` поверх axios) обрабатываются в местах вызова — в MobX-сторах (`module/Notes/stores/notes.ts`, `sendScreen.ts`) через `try/catch`.
## Замечания
- Путь `postScreen` (`/api/v1/nd/bound-note/{noteId}/`) не совпадает с фактическим маршрутом бэкенда `/api/v1/nd/nd_proxy/{instance_id}/bound/` — при интеграции стоит свериться с актуальным API notes-backend.
- Часть создания/обновления ссылок идёт через сервис `sarex` (`/api/core/target-links/`), а привязка ссылки к заметке — через `sarexApi` (`/notes/api/v1/links/`).
- В `endpoints.ts` присутствует закомментированный устаревший вариант `updateLink` (декларативный стиль `service/method/path/body`) — актуальна функция-обёртка над `httpService`.

737
apps/notes/openapi.yaml Normal file
View File

@ -0,0 +1,737 @@
openapi: 3.0.3
info:
title: Notes Service API
version: "0.0.1"
description: |
REST API сервиса **notes-backend** (`aero/notes-backend`) — управление
заметками к сущностям (workspace), их ссылками, документами и вложениями,
а также генерацией документов через workflow.
Сервис написан на Python (**FastAPI**, pydantic v1). Приложение собирается
фабрикой `get_app` в `src/app/main.py`. Публичный роутинг подключается с
префиксом `/api/v1` (`src/app/routers/__init__.py`) и включает группы
`notes`, `links`, `documents`, `attachments`.
Опционально (при `ENABLE_ND=true`) подключается роутер `nd_service` с
префиксом `/api/v1/nd` (`src/app/nd_service/router.py`).
### Аутентификация
Аутентификация выполняется middleware `DjangoUserMiddleware`
(`src/app/middleware.py`). При `DJANGO_USE=true` каждый запрос (кроме путей,
содержащих `/nd`) должен содержать заголовок `Authorization` — токен
проверяется обращением к Django `/client/settings/`. Опционально
передаётся заголовок `Identity` (режим Zitadel). Если заголовок
`Authorization` отсутствует — возвращается `401`.
При `DJANGO_USE=false` middleware подставляет тестового пользователя-
администратора (использовать только локально).
Дополнительно, права проверяются зависимостью `PermissionManager`:
для не-админов метод сопоставляется с правом (`base.can_add_note` для POST,
`base.can_view_note` для GET, `base.can_change_note` для PUT/PATCH,
`base.can_delete_note` для DELETE); при отсутствии права — `403`.
Часть эндпоинтов НД (`/api/v1/nd/nd_proxy/*` на запись) защищена
JWT (`JWTBearer`, включается флагом `ND_JWT_ENABLE`).
### Замечания (расхождения кода)
- Ошибки валидации тела/параметров (Pydantic) отдаются FastAPI в
стандартном формате `422`.
- Подключение к БД всегда идёт с `sslmode=verify-full` независимо от
значения `PG_SSL_MODE`.
- Многие пути завершаются слэшем (`/api/v1/notes/{id}/`).
contact:
name: notes-backend
url: https://gitlab/aero/notes-backend
servers:
- url: https://api.sarex.io/notes
description: Production (ingress, BASE_HOST)
- url: https://api.preprod.sarex.io/notes
description: Preprod (ingress, BASE_HOST)
- url: https://stage-api.sarex.io/notes
description: Stage (ingress, BASE_HOST)
- url: http://notes-backend-service:8000
description: Внутрикластерный адрес (ClusterIP)
- url: http://localhost:8000
description: Локальный запуск (gunicorn/docker, порт 8000)
tags:
- name: notes
description: Заметки — создание, просмотр, обновление, поиск, документы и вложения
- name: links
description: Ссылки, привязанные к заметкам
- name: documents
description: Документы, привязанные к заметкам
- name: attachments
description: Вложения заметок
- name: nd_service
description: Проксирование НД (подключается при ENABLE_ND=true)
security:
- bearerAuth: []
paths:
/api/v1/notes/:
post:
tags: [notes]
summary: Создать заметку
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BaseNote'
responses:
'201':
description: Создано
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'400':
description: Некорректный company_id
get:
tags: [notes]
summary: Список заметок
parameters:
- { name: search, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer, default: 1000, minimum: 0 } }
- { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } }
- { name: created_from, in: query, schema: { type: string, format: date-time } }
- { name: created_to, in: query, schema: { type: string, format: date-time } }
- { name: time_start, in: query, schema: { type: string, format: date-time } }
- { name: time_end, in: query, schema: { type: string, format: date-time } }
- { name: author, in: query, schema: { type: string } }
- { name: description, in: query, schema: { type: boolean } }
- { name: document, in: query, schema: { type: boolean } }
- { name: resource_id, in: query, description: "Список UUID через запятую", schema: { type: string } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
/api/v1/notes/{service}/{entity}/{instance_id}/:
get:
tags: [notes]
summary: Заметки по сервису/сущности/инстансу
parameters:
- { name: service, in: path, required: true, schema: { $ref: '#/components/schemas/Service' } }
- { name: entity, in: path, required: true, schema: { $ref: '#/components/schemas/Entity' } }
- { name: instance_id, in: path, required: true, schema: { type: string } }
- { name: full, in: query, description: "Вернуть расширенные заметки (ExtendedNote)", schema: { type: boolean, default: false } }
- { name: search, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer, default: 1000 } }
- { name: offset, in: query, schema: { type: integer, default: 0 } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
oneOf:
- $ref: '#/components/schemas/Note'
- $ref: '#/components/schemas/ExtendedNote'
/api/v1/notes/{instance_id}/:
get:
tags: [notes]
summary: Заметка по id
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Note' }
'404':
description: Не найдено
put:
tags: [notes]
summary: Обновить заметку
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BaseNote' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Note' }
'400': { description: Некорректный company_id }
'404': { description: Не найдено }
delete:
tags: [notes]
summary: Удалить заметку
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: Удалено (возвращает удалённый объект)
content:
application/json:
schema: { $ref: '#/components/schemas/Note' }
'404': { description: Не найдено }
/api/v1/notes/{instance_id}/documents/:
post:
tags: [notes]
summary: Добавить документы к заметке
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/BaseDocument' }
responses:
'201':
description: Создано
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Document' }
'404': { description: Заметка не найдена }
/api/v1/notes/{instance_id}/attachments/:
post:
tags: [notes]
summary: Загрузить файлы-вложения к заметке
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items: { type: string, format: binary }
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
type: object
properties:
name: { type: string }
link: { type: string }
id: { type: integer }
attachment_type: { type: string }
'404': { description: Заметка не найдена }
get:
tags: [notes]
summary: Вложения заметки
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: OK
content:
application/json:
schema: { type: array, items: { type: object } }
'404': { description: Заметка не найдена }
/api/v1/notes/{instance_id}/bound_attachments/:
post:
tags: [notes]
summary: Привязать существующее вложение к заметке
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BaseAttachment' }
responses:
'201':
description: Создано
content:
application/json:
schema: { $ref: '#/components/schemas/Attachment' }
'404': { description: Заметка не найдена }
/api/v1/notes/{instance_id}/generate_document/:
post:
tags: [notes]
summary: Сгенерировать документ по заметке (через workflow)
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DocumentCreation' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/WFResponse' }
'404': { description: Заметка не найдена }
/api/v1/links/:
post:
tags: [links]
summary: Создать ссылку
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BaseLink' }
responses:
'201':
description: Создано
content:
application/json:
schema: { $ref: '#/components/schemas/Link' }
'400': { description: Некорректный note_id }
get:
tags: [links]
summary: Список ссылок
parameters:
- { name: note, in: query, description: "Фильтр по note_id", schema: { type: integer } }
- { name: limit, in: query, schema: { type: integer, default: 1000 } }
- { name: offset, in: query, schema: { type: integer, default: 0 } }
responses:
'200':
description: OK
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/Link' } }
/api/v1/links/{instance_id}/:
get:
tags: [links]
summary: Ссылка по id
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Link' }
'404': { description: Не найдено }
put:
tags: [links]
summary: Обновить ссылку
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BaseLink' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Link' }
'404': { description: Не найдено }
delete:
tags: [links]
summary: Удалить ссылку
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: Удалено
content:
application/json:
schema: { $ref: '#/components/schemas/Link' }
'404': { description: Не найдено }
/api/v1/documents/{instance_id}/:
delete:
tags: [documents]
summary: Удалить документ заметки
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: Удалено
content:
application/json:
schema: { $ref: '#/components/schemas/Document' }
'404': { description: Не найдено }
/api/v1/attachments/{instance_id}/:
delete:
tags: [attachments]
summary: Удалить вложение
parameters:
- { name: instance_id, in: path, required: true, description: "attachment_id", schema: { type: integer } }
responses:
'200':
description: Удалено
content:
application/json:
schema: { $ref: '#/components/schemas/Attachment' }
'404': { description: Не найдено }
/api/v1/nd/nd_proxy/:
post:
tags: [nd_service]
summary: Создать НД-прокси
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxyCreate' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxySchema' }
'400': { description: Нарушение целостности }
get:
tags: [nd_service]
summary: Список НД-прокси
security: [{ bearerAuth: [] }]
parameters:
- { name: is_bound, in: query, schema: { type: boolean } }
- { name: limit, in: query, schema: { type: integer, default: 1000 } }
- { name: offset, in: query, schema: { type: integer, default: 0 } }
responses:
'200':
description: OK
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/NDProxySchema' } }
/api/v1/nd/nd_proxy/{instance_id}/:
get:
tags: [nd_service]
summary: НД-прокси по коду
parameters:
- { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxySchema' }
'404': { description: Не найдено }
put:
tags: [nd_service]
summary: Обновить НД-прокси
parameters:
- { name: instance_id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxyCreate' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxySchema' }
'404': { description: Не найдено }
patch:
tags: [nd_service]
summary: Частично обновить НД-прокси
parameters:
- { name: instance_id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxyUpdate' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxySchema' }
'404': { description: Не найдено }
delete:
tags: [nd_service]
summary: Удалить НД-прокси
parameters:
- { name: instance_id, in: path, required: true, schema: { type: string } }
responses:
'200':
description: Удалено
content:
application/json:
schema: { $ref: '#/components/schemas/NDProxySchema' }
'404': { description: Не найдено }
/api/v1/nd/nd_proxy/{instance_id}/bound/:
post:
tags: [nd_service]
summary: Привязать заметку и файл к НД-прокси
parameters:
- { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
note_id: { type: integer }
name: { type: string }
file: { type: string, format: binary }
responses:
'200':
description: OK
'404': { description: Не найдено }
/api/v1/nd/notes/{instance_id}/:
get:
tags: [nd_service]
summary: Заметка НД с изображением
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: OK
content:
application/json:
schema: { type: object }
'404': { description: Не найдено }
patch:
tags: [nd_service]
summary: Обновить время заметки (НД)
parameters:
- { name: instance_id, in: path, required: true, schema: { type: integer } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NDUpdateTimeNote' }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/Note' }
'404': { description: Не найдено }
/api/v1/nd/users/:
post:
tags: [nd_service]
summary: Проверить существование пользователя по username
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
exist: { type: boolean }
'400': { description: Ошибка запроса к Django }
/api/v1/nd/token/:
get:
tags: [nd_service]
summary: Сгенерировать JWT для НД-сервиса
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
token: { type: string }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: |
Заголовок `Authorization` (проверяется через Django `/client/settings/`).
Опционально заголовок `Identity` для режима Zitadel. Эндпоинты `/api/v1/nd/*`
на запись используют собственный JWT (`JWTBearer`, флаг ND_JWT_ENABLE).
schemas:
Service:
type: string
enum: [workspace]
Entity:
type: string
enum: [workspace]
BaseNote:
type: object
required: [name, service, entity, instance_id, company_id]
properties:
name: { type: string }
body: { type: string, nullable: true }
time_start: { type: string, format: date-time, nullable: true }
time_end: { type: string, format: date-time, nullable: true }
service: { $ref: '#/components/schemas/Service' }
entity: { $ref: '#/components/schemas/Entity' }
instance_id: { type: string }
meta_data: { type: object, nullable: true }
height_base_plane: { type: number, format: float, nullable: true }
height_geom_prmtv: { type: number, format: float, nullable: true }
created_by: { type: integer, nullable: true, minimum: 1 }
company_id: { type: integer, minimum: 1 }
resource_id: { type: string, format: uuid, nullable: true }
Note:
allOf:
- $ref: '#/components/schemas/BaseNote'
- type: object
required: [id, created_at, updated_at]
properties:
id: { type: integer }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
ExtendedNote:
allOf:
- $ref: '#/components/schemas/Note'
- type: object
properties:
links:
type: array
items: { type: integer }
documents:
type: array
items: { $ref: '#/components/schemas/Document' }
DocumentCreation:
type: object
required: [path, values, file_name, template]
properties:
path: { type: string }
values: { type: object, additionalProperties: true }
file_name: { type: string }
template: { type: string }
WFResponse:
type: object
required: [context]
properties:
context: { type: object }
BaseLink:
type: object
required: [link_id, note_id]
properties:
link_id: { type: integer, minimum: 1 }
note_id: { type: integer, minimum: 1 }
Link:
allOf:
- $ref: '#/components/schemas/BaseLink'
- type: object
required: [id]
properties:
id: { type: integer }
BaseDocument:
type: object
required: [document_id, bundle_id]
properties:
document_id: { type: integer }
bundle_id: { type: string }
note_id: { type: integer, nullable: true }
Document:
allOf:
- $ref: '#/components/schemas/BaseDocument'
- type: object
required: [id]
properties:
id: { type: integer }
BaseAttachment:
type: object
required: [attachment_id, object_name, attachment_type, is_state]
properties:
attachment_id: { type: integer, minimum: 1 }
note_id: { type: integer, nullable: true, minimum: 1 }
object_name: { type: string }
attachment_type: { type: string }
is_state: { type: boolean }
Attachment:
allOf:
- $ref: '#/components/schemas/BaseAttachment'
- type: object
required: [id]
properties:
id: { type: integer }
NDProxyCreate:
type: object
required: [nd_code, creator, is_locked]
properties:
nd_code: { type: string }
work_description: { type: string, nullable: true }
equipment_description: { type: string, nullable: true }
description: { type: string, nullable: true }
creator: { type: string }
is_locked: { type: boolean }
note_id: { type: integer, nullable: true }
NDProxyUpdate:
type: object
properties:
nd_code: { type: string, nullable: true }
work_description: { type: string, nullable: true }
equipment_description: { type: string, nullable: true }
description: { type: string, nullable: true }
creator: { type: string, nullable: true }
is_locked: { type: boolean, nullable: true }
note_id: { type: integer, nullable: true }
NDProxySchema:
allOf:
- $ref: '#/components/schemas/NDProxyCreate'
- type: object
required: [id]
properties:
id: { type: integer }
NDUpdateTimeNote:
type: object
properties:
time_start: { type: string, format: date-time, nullable: true }
time_end: { type: string, format: date-time, nullable: true }
User:
type: object
required: [username]
properties:
username: { type: string }

134
apps/pm/.env.example Normal file
View File

@ -0,0 +1,134 @@
# Server
SERVER_HOST="https://lk.sarex.io"
SERVER_API_HOST="https://api.sarex.io"
SERVER_DEBUG=false
SERVER_ENABLE_SILK=false
SERVER_ALLOWED_HOSTS=["*"]
SERVER_SECRET_KEY="secret"
SERVER_USE_OTEL=false
SERVER_VERIFY_SSL=true
SERVER_LOG_LEVEL=INFO
SERVER_ENABLE_SYNC_RESOURCES=false
# SERVER_MEDIA_ROOT=sarex/media
SERVER_DELETED_TASK_MAX_AGE_DAYS=30
SERVER_EXPIRED_TASK_NOTIFICATION_HOUR=9
SERVER_EXPIRED_TASK_NOTIFICATION_MAX_DAYS=7
SERVER_SEND_UPDATED_PROJECTS_INFO_INTERVAL=5
# Auth
AUTH_ALGORITHM=RS512
# Replace newlines with \n
AUTH_PUBLIC_KEY=''
AUTH_PUBLIC_TOKEN_URL="https://lk.sarex.io/api/token/public/"
# Database (PostgreSQL)
DB_ENGINE="django.db.backends.postgresql"
DB_HOST="localhost"
DB_PORT=5432
DB_DATABASE="sarex_db"
DB_USERNAME="sarex"
DB_PASSWORD="sarex"
# S3
S3_HOST="https://storage.yandexcloud.net"
S3_LOGIN=""
S3_PASSWORD=""
S3_BUCKET="sarex-media-storage"
S3_VERIFY="true"
# Cache (Redis)
CACHE_ENABLE=0
CACHE_EXPIRATION=300
CACHE_HOST="localhost"
CACHE_PORT=6379
CACHE_PASSWORD=None
CACHE_SSL=0
CACHE_SSL_CA_CERTS=None
CACHE_QUEUE=default
# ClickHouse
CLICKHOUSE_ENABLE=0
CLICKHOUSE_HOST="localhost"
CLICKHOUSE_PORT=9000
CLICKHOUSE_USER=""
CLICKHOUSE_PASSWORD=""
CLICKHOUSE_DATABASE="values_db"
CLICKHOUSE_TABLE="values"
CLICKHOUSE_SECURE=0
CLICKHOUSE_VERIFY=0
CLICKHOUSE_CERT=""
# Kafka
KAFKA_ENABLE=0
KAFKA_BOOTSTRAP_SERVERS=["localhost:9092"]
KAFKA_SECURITY_PROTOCOL=""
KAFKA_SASL_MECHANISM=""
KAFKA_SASL_PLAIN_USERNAME="user"
KAFKA_SASL_PLAIN_PASSWORD="password"
KAFKA_SSL_CAFILE=""
KAFKA_TOPICS={"planning": "message-hub-stage"}
# Celery — RabbitMQ (broker)
CELERY_RABBITMQ_HOST='localhost'
CELERY_RABBITMQ_PORT=5672
CELERY_RABBITMQ_USER='rabbit'
CELERY_RABBITMQ_PASSWORD='rabbit'
CELERY_RABBITMQ_VHOST="pm"
# Celery — Redis (result backend)
CELERY_REDIS_HOST='redis-service.sarex-stage.svc.cluster.local'
CELERY_REDIS_PORT=6379
CELERY_REDIS_DATABASE=0
# CELERY_REDIS_PASSWORD=
CELERY_REDIS_SSL=false
# CELERY_REDIS_SSL_CA_CERTS=
CELERY_REDIS_SSL_CERT_REQS=required
# Users service
USERS_HOST=https://lk.sarex.io
USERS_API_PREFIX=/api/core
USERS_INTERNAL_HOST=http://backend-service.sarex-stage.svc.cluster.local:8000
USERS_INTERNAL_PREFIX=/internal
USERS_TIMEOUT=10
USERS_ENABLE=true
# Resources service (IAM/resources)
RESOURCES_INTERNAL_HOST=http://sarex-resources-service.resources-stage
RESOURCES_INTERNAL_PREFIX=/api/v1
RESOURCES_TIMEOUT=10
RESOURCES_ENABLE=true
# EAV service
EAV_HOST=http://eav-service.eav-stage
EAV_API_PREFIX=/api/v0
EAV_API_PREFIX_V1=/api/v1
EAV_TIMEOUT=10
EAV_ENABLE=true
# Gateway service
# GATEWAY_HOST=https://api.sarex.io
GATEWAY_API_PREFIX=/gateway/api/v1
GATEWAY_TIMEOUT=10
GATEWAY_ENABLE=true
# Documentation service
# DOCUMENTATION_HOST=https://api.sarex.io
DOCUMENTATION_API_PREFIX=/documentations/api/v1
DOCUMENTATION_TIMEOUT=10
DOCUMENTATION_ENABLE=true
# Tracing (OpenTelemetry) — used when SERVER_USE_OTEL=true
TRACING_SERVICE_NAME=pm-backend.pm-pord
TRACING_ENDPOINT=localhost:4317
TRACING_INSECURE=false
TRACING_ENVIRONMENT=prod
TRACING_MODULE=planning
TRACING_TEAM=team_planning
TRACING_COMPONENT=backend
# Sentry
SENTRY_USE=true
SENTRY_HOST=''
SENTRY_ENVIRONMENT=""
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=0.1

280
apps/pm/CONFIGURATION.md Normal file
View File

@ -0,0 +1,280 @@
# Конфигурация проекта pm-backend
Документ описывает все переменные окружения и способы конфигурирования сервиса.
## Способы конфигурирования
Сервис — это Django-приложение (Django 5.1 + Django REST Framework), запускаемое как ASGI (`config.asgi_root:application`) через gunicorn с воркерами `uvicorn.workers.UvicornWorker`. Настройки читаются из переменных окружения через набор классов [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), объявленных в `config/settings/base.py` и `config/settings/deps/*`.
Особенности разбора:
- **у каждой секции свой префикс** (`env_prefix`), напр. `SERVER_`, `DB_`, `S3_`, `CACHE_`, `CLICKHOUSE_`, `KAFKA_`, `CELERY_RABBITMQ_`, `CELERY_REDIS_`, `AUTH_`, `GATEWAY_`, `EAV_`, `DOCUMENTATION_`, `USERS_`, `RESOURCES_`, `TRACING_`, `SENTRY_`;
- **вложенного делимитера нет** — каждая настройка задаётся плоской переменной вида `<PREFIX><FIELD>`, напр. `DB_HOST`, `CELERY_RABBITMQ_VHOST`;
- **`extra='ignore'`** — все классы игнорируют посторонние переменные, поэтому один общий `.env` без ошибок разбирается всеми секциями;
- **`env_file='.env'`** — в отличие от эталонного сервиса, здесь `.env` **загружается автоматически** каждым классом настроек (у `SentrySettings``.env.base` и `.env`). Значения из реального окружения процесса имеют приоритет над файлом.
Отдельного конфиг-файла (yaml/toml) у приложения нет. `DJANGO_SETTINGS_MODULE` по умолчанию — `config.settings.base` (см. `manage.py`).
Источники переменных по способам запуска:
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Локально (manage.py / gunicorn) | Переменные окружения процесса + файл `.env` в корне репозитория (загружается pydantic-settings) |
| Локально (docker-compose) | `docker-compose.yaml` поднимает зависимости (postgres, redis, rabbit, clickhouse, minio, pgadmin); переменные приложения задаются через окружение/`.env` |
| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) для сервисов `api` и `celery`; чарт-зависимость `universal-chart` |
| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна (`workflow.rules`) и job-ы `linter`/`typechecker`/`linter_src` |
Способы запуска процессов:
| Процесс | Точка входа | Назначение |
| --- | --- | --- |
| HTTP API | `docker/entrypoint.sh``gunicorn config.asgi_root:application` (uvicorn worker, порт 8000) | REST API |
| Celery worker/beat | `celery -A config worker -B -Q pm …` (см. `.helm/values.yaml`, сервис `celery`) | Фоновые задачи и периодические (beat) |
| `manage.py migrate` | `manage.py` | Миграции БД |
| `manage.py clean_db` / `import_data` | `manage.py` | Служебные команды (см. `README.md`) |
## Переменные приложения
Ниже перечислены все секции настроек с их префиксами. Дефолт `—` означает отсутствие значения по умолчанию в коде.
### Server (`SERVER_*`)
Класс `ServerSettings` (`config/settings/base.py`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SERVER_HOST` | string | `https://lk.sarex.io` | Внешний базовый URL (личный кабинет); из него формируется `SERVER_MEDIA_HOST` |
| `SERVER_API_HOST` | string | `https://api.sarex.io` | Базовый URL API-шлюза (используется внешними клиентами по умолчанию) |
| `SERVER_MEDIA_ROOT` | string | `sarex/media` | Каталог медиафайлов |
| `SERVER_DEBUG` | bool | `False` | Django DEBUG. При `True` также включает `FAKE_CELERY` и обход аутентификации в `JWTAuthentication` |
| `SERVER_ENABLE_SILK` | bool | `False` | Подключить профайлер django-silk (только при `DEBUG`) |
| `SERVER_ALLOWED_HOSTS` | list[str] (JSON) | `["*"]` | Django `ALLOWED_HOSTS` |
| `SERVER_SECRET_KEY` | string | `secret` | Django `SECRET_KEY` |
| `SERVER_USE_OTEL` | bool | `False` | Включить OpenTelemetry-трейсинг и OTel-логгер (см. секцию `TRACING_*`) |
| `SERVER_VERIFY_SSL` | bool | `True` | Проверять TLS-сертификаты при обращении к внешним сервисам |
| `SERVER_LOG_LEVEL` | enum | `INFO` | `DEBUG`/`INFO`/`WARNING`/`CRITICAL`/`FATAL` |
| `SERVER_ENABLE_SYNC_RESOURCES` | bool | `False` | Включить синхронизацию ресурсов |
| `SERVER_DELETED_TASK_MAX_AGE_DAYS` | int | `30` | Срок хранения удалённых задач (дней) |
| `SERVER_EXPIRED_TASK_NOTIFICATION_HOUR` | int | `9` | Час отправки уведомлений о просроченных задачах |
| `SERVER_EXPIRED_TASK_NOTIFICATION_MAX_DAYS` | int | `7` | Горизонт уведомлений о просрочке (дней) |
| `SERVER_SEND_UPDATED_PROJECTS_INFO_INTERVAL` | int | `5` | Интервал отправки информации об обновлённых проектах |
### Auth (`AUTH_*`)
Класс `AUTHSettings` (`config/settings/deps/auth.py`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `AUTH_ALGORITHM` | string | `RS512` | Алгоритм проверки подписи JWT |
| `AUTH_PUBLIC_KEY` | string | `''` | Публичный RSA-ключ для проверки JWT в режиме sarex-backend |
| `AUTH_PUBLIC_TOKEN_URL` | string | `https://lk.sarex.io/api/token/public/` | URL получения публичного ключа/токена |
Аутентификация DRF (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`) — по очереди `ZitadelJWTAuthentication`, затем `JWTAuthentication`; доступ по умолчанию `IsAuthenticated`. Zitadel-режим требует одновременно заголовки `Authorization` и `Identity`.
### Database (`DB_*`)
Класс `DBSettings` (`config/settings/deps/db.py`). PostgreSQL.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DB_ENGINE` | string | `django.db.backends.postgresql` | Движок Django ORM |
| `DB_HOST` | string | `localhost` | Хост PostgreSQL |
| `DB_PORT` | int | `5432` | Порт PostgreSQL |
| `DB_DATABASE` | string | `sarex_db` | Имя базы данных |
| `DB_USERNAME` | string | `sarex` | Пользователь БД |
| `DB_PASSWORD` | string | `sarex` | Пароль пользователя БД |
### S3 (`S3_*`)
Класс `S3Settings` (`config/settings/deps/s3.py`). Хранилище через django-storages (boto3), по умолчанию Yandex Object Storage.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_HOST` | string | `https://storage.yandexcloud.net` | Endpoint S3 |
| `S3_LOGIN` | string | `''` | Access key |
| `S3_PASSWORD` | string | `''` | Secret key |
| `S3_BUCKET` | string | `sarex-media-storage` | Бакет по умолчанию |
| `S3_VERIFY` | bool | `True` | Проверять TLS-сертификат |
### Cache (`CACHE_*`)
Класс `CacheSettings` (`config/settings/deps/cache.py`). Redis-кеш, включается отдельно.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CACHE_ENABLE` | bool | `False` | Включить кеш (иначе `CACHE_CLIENT=None`) |
| `CACHE_EXPIRATION` | int | `300` | TTL записей (сек) |
| `CACHE_HOST` | string | `localhost` | Хост Redis |
| `CACHE_PORT` | int | `6379` | Порт Redis |
| `CACHE_PASSWORD` | string \| null | `None` | Пароль |
| `CACHE_SSL` | bool | `False` | Подключение по TLS |
| `CACHE_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату |
| `CACHE_QUEUE` | string | `default` | Имя очереди кеша |
### ClickHouse (`CLICKHOUSE_*`)
Класс `ClickHouseSettings` (`config/settings/deps/click_house.py`). Хранилище значений, включается отдельно.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CLICKHOUSE_ENABLE` | bool | `False` | Включить ClickHouse |
| `CLICKHOUSE_HOST` | string | `rc1d-…​.mdb.yandexcloud.net` | Хост |
| `CLICKHOUSE_PORT` | int | `9000` | Порт |
| `CLICKHOUSE_USER` | string | `''` | Пользователь |
| `CLICKHOUSE_PASSWORD` | string | `''` | Пароль |
| `CLICKHOUSE_DATABASE` | string | `values_db` | База данных |
| `CLICKHOUSE_TABLE` | string | `values` | Таблица |
| `CLICKHOUSE_SECURE` | bool | `False` | Защищённое подключение |
| `CLICKHOUSE_VERIFY` | bool | `False` | Проверять сертификат |
| `CLICKHOUSE_CERT` | string | `''` | Путь к CA-сертификату |
### Kafka (`KAFKA_*`)
Класс `KafkaSettings` (`config/settings/deps/kafka.py`). Продюсер сообщений, включается отдельно.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `KAFKA_ENABLE` | bool | `False` | Включить продюсер (иначе `get_producer()` вернёт `None`) |
| `KAFKA_BOOTSTRAP_SERVERS` | list[str] (JSON) | `["localhost:9092"]` | Список брокеров |
| `KAFKA_SECURITY_PROTOCOL` | string | `''` | Протокол безопасности |
| `KAFKA_SASL_MECHANISM` | string | `''` | SASL-механизм |
| `KAFKA_SASL_PLAIN_USERNAME` | string | `user` | SASL-логин |
| `KAFKA_SASL_PLAIN_PASSWORD` | string | `password` | SASL-пароль |
| `KAFKA_SSL_CAFILE` | string | `''` | Путь к CA-сертификату |
| `KAFKA_TOPICS` | dict (JSON) | `{}` | Карта топиков, напр. `{"planning": "message-hub-stage"}` |
### Celery — RabbitMQ (`CELERY_RABBITMQ_*`)
Класс `CeleryRabbitMQ` (`config/settings/deps/celery.py`). Брокер задач; из полей собирается `BROKER_URL` (`amqp://…?heartbeat=30`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CELERY_RABBITMQ_HOST` | string | `localhost` | Хост |
| `CELERY_RABBITMQ_PORT` | int | `5672` | Порт |
| `CELERY_RABBITMQ_USER` | string | `rabbit` | Пользователь |
| `CELERY_RABBITMQ_PASSWORD` | string | `rabbit` | Пароль |
| `CELERY_RABBITMQ_VHOST` | string | `api` | Виртуальный хост (в `.env`/helm — `pm`) |
### Celery — Redis (`CELERY_REDIS_*`)
Класс `CeleryRedis` (`config/settings/deps/celery.py`). Result backend; при `SSL=true` используется `rediss://` и `ssl_cert_reqs`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CELERY_REDIS_HOST` | string | `redis` | Хост |
| `CELERY_REDIS_PORT` | int | `6379` | Порт |
| `CELERY_REDIS_DATABASE` | int | `0` | Номер БД Redis |
| `CELERY_REDIS_PASSWORD` | string \| null | `None` | Пароль (используется при SSL) |
| `CELERY_REDIS_SSL` | bool | `False` | Подключение по TLS (`rediss://`) |
| `CELERY_REDIS_SSL_CA_CERTS` | string \| null | `None` | Путь к CA-сертификату |
| `CELERY_REDIS_SSL_CERT_REQS` | string \| null | `required` | Требования к сертификату |
### HTTP-клиенты внешних сервисов
Общий базовый класс `BaseApiServiceMixin` (`config/settings/base.py`): поля `host` (по умолчанию `SERVER_API_HOST`), `api_prefix`, `internal_host`, `internal_prefix`, `timeout` (`10`), `enable` (`True`). Наследники задают собственные префиксы и дефолтные значения prefix.
| Секция / префикс | Класс | Назначение | Особенности |
| --- | --- | --- | --- |
| `GATEWAY_*` | `GateWaySetttings` | API-шлюз | `api_prefix=/gateway/api/v1` |
| `EAV_*` | `EAVSettings` | Сервис атрибутов (EAV) | `api_prefix=/eav/api/v0`, доп. `EAV_API_PREFIX_V1=/eav/api/v1` |
| `DOCUMENTATION_*` | `DocumentationSettings` | Сервис документаций | `api_prefix=/documentations/api/v1` |
| `USERS_*` | `UsersSettings` | Сервис пользователей (core) | `host=SERVER_HOST`, `api_prefix=/api/core`, `internal_host=http://localhost:8001`, `internal_prefix=/internal` |
| `RESOURCES_*` | `ResourceSettings` | Сервис ресурсов (IAM) | `internal_host=http://localhost:8001`, `internal_prefix=/api/v1` |
Для каждого клиента доступны переменные `<PREFIX>HOST`, `<PREFIX>API_PREFIX`, `<PREFIX>INTERNAL_HOST`, `<PREFIX>INTERNAL_PREFIX`, `<PREFIX>TIMEOUT`, `<PREFIX>ENABLE` (плюс `EAV_API_PREFIX_V1`).
### Tracing / OpenTelemetry (`TRACING_*`)
Класс `TracingConfig` (`config/settings/base.py`). Применяется только при `SERVER_USE_OTEL=true`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRACING_SERVICE_NAME` | string | `pm-backend.pm-pord` | Имя сервиса в трейсах |
| `TRACING_ENDPOINT` | string | `localhost:4317` | Адрес OTLP-коллектора |
| `TRACING_INSECURE` | bool | `False` | Небезопасное (без TLS) подключение |
| `TRACING_ENVIRONMENT` | string | `prod` | Окружение (атрибут трейса) |
| `TRACING_MODULE` | string | `planning` | Модуль (атрибут трейса) |
| `TRACING_TEAM` | string | `team_planning` | Команда (атрибут трейса) |
| `TRACING_COMPONENT` | string | `backend` | Компонент (атрибут трейса) |
### Sentry (`SENTRY_*`)
Класс `SentrySettings` (`config/settings/deps/sentry.py`). Читает `.env.base` и `.env`. Инициализируется при `SENTRY_USE=true`.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `SENTRY_USE` | bool | `True` | Включить Sentry |
| `SENTRY_HOST` | string | `''` | DSN Sentry |
| `SENTRY_ENVIRONMENT` | string | `''` | Окружение |
| `SENTRY_TRACES_SAMPLE_RATE` | float | `1.0` | Доля трейсов |
| `SENTRY_PROFILES_SAMPLE_RATE` | float | `0.1` | Доля профилей |
## Переменные инфраструктуры, сборки и деплоя
Не читаются кодом приложения напрямую, но участвуют в запуске/сборке/деплое.
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `GUNICORN_WORKERS` | `docker/entrypoint.sh` | Число воркеров gunicorn (по умолчанию `4`) |
| `TIMEOUT` | `docker/entrypoint.sh` | Таймаут воркера gunicorn (по умолчанию `60`) |
| `NEXUS_USERNAME`, `NEXUS_PASSWORD` | `docker/Dockerfile` (build-arg), `.gitlab-ci.yml` | Доступ к приватному индексу пакетов Nexus |
| `SETTINGS_BASE_HOST` | `.helm/values.yaml` (env) | Базовый хост окружения (`stage`/`preprod`/`lk`) |
Порядок запуска контейнера (`docker/entrypoint.sh`): миграции закомментированы, сразу стартует gunicorn с ASGI-приложением `config.asgi_root:application`.
## Переменные из Helm-чарта (`.helm/values.yaml`)
Чарт зависит от `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`). Значения задаются для двух сервисов — `api` и `celery`с ключами по окружениям `_default`/`stage`/`preprod`/`production`.
Обычные значения (блок `envs`) включают: `USERS_INTERNAL_HOST`, `CELERY_REDIS_HOST`, `RESOURCES_INTERNAL_HOST`, `EAV_HOST`, `EAV_API_PREFIX`, `EAV_API_PREFIX_V1`, `TRACING_ENDPOINT`, `TRACING_INSECURE`, `SERVER_ENABLE_SYNC_RESOURCES`, `SERVER_DELETED_TASK_MAX_AGE_DAYS`, `SERVER_EXPIRED_TASK_NOTIFICATION_HOUR`, `SETTINGS_BASE_HOST` (различаются адресами сервисов по окружениям).
Значения из секретов (блок `secretEnvs`, монтируются как env через `secretKeyRef`):
| Секрет (`secretName`) | Переменные |
| --- | --- |
| `ya-pg-secret-pm` | `DB_USERNAME`, `DB_PASSWORD`, `DB_DATABASE`, `DB_HOST`, `DB_PORT` |
| `ya-s3-secret-pm` | `S3_HOST`, `S3_LOGIN`, `S3_PASSWORD`, `S3_BUCKET` |
| `cache-secret-pm` | `CACHE_HOST`, `CACHE_PORT`, `CACHE_PASSWORD`, `CACHE_SSL`, `CACHE_SSL_CA_CERTS`, `CACHE_ENABLE` |
| `clickhouse-secret-pm` | `CLICKHOUSE_HOST`, `CLICKHOUSE_PORT`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CLICKHOUSE_DATABASE`, `CLICKHOUSE_TABLE`, `CLICKHOUSE_SECURE`, `CLICKHOUSE_VERIFY`, `CLICKHOUSE_CERT`, `CLICKHOUSE_ENABLE` |
| `ya-kafka-secret-pm` | `KAFKA_ENABLE`, `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_SECURITY_PROTOCOL`, `KAFKA_SASL_MECHANISM`, `KAFKA_SASL_PLAIN_USERNAME`, `KAFKA_SASL_PLAIN_PASSWORD`, `KAFKA_SSL_CAFILE`, `KAFKA_TOPICS` |
| `rabbit-secret-pm` | `CELERY_RABBITMQ_HOST`, `CELERY_RABBITMQ_PORT`, `CELERY_RABBITMQ_USER`, `CELERY_RABBITMQ_PASSWORD`, `CELERY_RABBITMQ_VHOST` |
| `server-secret-pm` | `AUTH_PUBLIC_TOKEN_URL`, `SERVER_HOST`, `SERVER_API_HOST`, `SERVER_DEBUG`, `SERVER_ALLOWED_HOSTS`, `SERVER_VERIFY_SSL`, `SERVER_LOG_LEVEL` |
Дополнительно чарт монтирует CA-сертификат ClickHouse (configMap `ch-cert`, ключ `CA.pem`) как файл `/root/clickhouse/RootCA.crt` и `tmp-volume` в `/tmp`. Прочие значения чарта (не переменные приложения): `deployment.*` (имя, реплики, ресурсы, probes на `/api/health/`), `image.*`, `service.*`, `affinity`, `owner`.
## Переменные в CI (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | NAMESPACE | CHART_VERSION |
| --- | --- | --- | --- |
| ветка `stage` | `stage` | `planning` | `0.0.1-stage` |
| ветка `master` | `preprod` | `pm-preprod` | `0.0.1-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `pm-prod` | `0.0.1-prod` |
| merge request | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Ключевые переменные пайплайна: `SERVICE_NAME=pm-backend`, `DOCKERFILE_PATH=docker/Dockerfile`, `RELEASE_NAME`, `CHART_NAME`, `K8S_HUSTLER_BRANCH`, `IMAGE_NAME`, `HELM_SET_ARGS` (`--set universal-chart.services.{api,celery}.image.name…` и метаданные коммита). Джобы стадии `test`: `linter` (flake8 по `sarex`), `typechecker` (mypy по `src`), `linter_src` (ruff check/format по `src`).
## Замечания и потенциальные проблемы
- При `SERVER_DEBUG=true` `JWTAuthentication` возвращает анонимного пользователя и **аутентификация обходится** — использовать только локально.
- Все секции читают `.env` автоматически (`env_file='.env'`), поэтому один общий `.env` в корне достаточен для локального запуска. `SentrySettings` дополнительно читает `.env.base`.
- Sentry инициализируется по умолчанию (`SENTRY_USE=true`), но при пустом `SENTRY_HOST` DSN не задан — задайте `SENTRY_USE=false` локально, чтобы отключить.
- `CELERY_RABBITMQ_VHOST` в коде по умолчанию `api`, тогда как в `.env.example`/helm используется `pm` — для корректной работы очереди значение должно совпадать с брокером.
- Переменные `CACHE_PASSWORD`/`CACHE_SSL_CA_CERTS`/`CELERY_REDIS_PASSWORD` допускают `None`; в `.env` для «пустого» значения используйте `None` или закомментируйте строку.
- Список-переменные (`SERVER_ALLOWED_HOSTS`, `KAFKA_BOOTSTRAP_SERVERS`) и dict (`KAFKA_TOPICS`) задаются в формате JSON.
## Минимальный набор для локального запуска
Зависимости (postgres, redis, rabbit, clickhouse, minio) поднимаются через `docker-compose up`. Минимально необходимо задать:
- `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`
- `S3_HOST`, `S3_BUCKET`, `S3_LOGIN`, `S3_PASSWORD`, `S3_VERIFY`
- `CELERY_RABBITMQ_*` (host/port/user/password/vhost) и `CELERY_REDIS_HOST`/`CELERY_REDIS_PORT`
- `SERVER_DEBUG=true` (локально), `SERVER_ALLOWED_HOSTS`, `SERVER_LOG_LEVEL`
- `AUTH_PUBLIC_KEY` (можно пустой при `SERVER_DEBUG=true`)
- адреса внешних сервисов при необходимости: `USERS_INTERNAL_HOST`, `RESOURCES_INTERNAL_HOST`, `EAV_HOST`
- `SENTRY_USE=false`, `SERVER_USE_OTEL=false` — чтобы не подключать Sentry/OTel локально
- опциональные подсистемы по флагам: `CACHE_ENABLE`, `CLICKHOUSE_ENABLE`, `KAFKA_ENABLE` (`0` по умолчанию)
Готовые значения-примеры приведены в `.env.example`.

229
apps/pm/ENDPOINTS.md Normal file
View File

@ -0,0 +1,229 @@
# Эндпоинты, с которыми взаимодействует pm-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `pm-frontend`).
## Как устроено взаимодействие
Все REST-запросы идут через единый `httpService` (`src/services/api/http-service.ts`, поверх `@sarex-team/sdk-js` + axios). API-модули объявлены декларативно в `src/store/api/*` и `src/services/api/*` и вызывают `httpService.<method>Request({ service, url, data?, axiosConfig?, errorMessage? })`, где:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (обычно уже включает свой префикс, напр. `/api/pm/msp/...`);
- `data` — тело запроса; `errorMessage` — сообщение при ошибке.
Базовый хост подставляется SDK-функцией `resolveHost(service)` по значению `hosts[ENDPOINT].hosts[service]` из `src/services/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url`. Прямых вызовов `axios.*`/`fetch()` в `src/` нет.
Выбор окружения — сборочная переменная `process.env.ENDPOINT` (инъектируется webpack через `DefinePlugin`). Допустимые значения: `local`, `stage`, `prod`, `preprod`, `contour`; значение по умолчанию — `prod`. Для `local`/`stage` dev-сервер webpack (`configWebpack/buildDevServer.ts`) проксирует относительные префиксы (`/sarex-backend`, `/pm`, `/sarex-eav-v1`, `/sarex-gateway`, `/sarex-api` …) на stage-бэкенды.
## Базовые хосты по сервисам и окружениям
Значения из `src/services/api/hosts.ts`. Для сервиса `sarex` в удалённых окружениях хост пустой (`""`) — запросы идут относительно текущего origin (маршрутизируются ingress/gateway перед SPA).
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `sarex` | Монолит / PM REST (`/api/pm/...`, `/api/core/...`) | `""` (same-origin) | `""` (same-origin) |
| `pm` | PM-микросервис (`/api/v1/...`: интегрированные задачи, комментарии) | `https://stage-api.sarex.io/pm` | `https://api.sarex.io/pm` |
| `sarexApi` | API-шлюз для flows (reviews, документы, процессы) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `eavV1` | Сервис атрибутов EAV (`/api/v2`, `/api/v4`) | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` |
| `gateway` | Шлюз ресурсов (`/api/v1/resources`) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
| `notifications` | Лямбда уведомлений | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` |
| `bimv2` | BIM v2 | `https://stage-api.sarex.io/bimv2` | `https://api.sarex.io/bimv2` |
| `bim` | BIM v1 | `https://stage-api.sarex.io/bim` | `https://api.sarex.io/bim` |
| `analyticsV2` | Аналитика v2 | `https://stage-api.sarex.io/analytics-v2` | `https://api.sarex.io/analytics-v2` |
| `workspaces` | Сервис рабочих областей | `https://stage-api.sarex.io/workspaces` | `https://api.sarex.io/workspaces` |
| `documentations` | Сервис документаций | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` |
| `workflows` | Сервис обработки документов | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` |
| `comparisons` | Сервис сравнений | `https://stage-api.sarex.io/comparisons` | `https://api.sarex.io/comparisons` |
| `remarks` | Сервис замечаний | `https://stage-api.sarex.io/remarks` | `https://api.sarex.io/remarks` |
| `projects` | Сервис проектов | `https://stage-api.sarex.io/projects` | `https://api.sarex.io/projects` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
| `google` | Временное хранилище (GCS) | `https://storage.googleapis.com/srx-tmp` | `https://storage.googleapis.com/srx-tmp` |
> Также определены окружения `local` (относительные прокси-префиксы) и `preprod`/`contour`. Реально используются в коде только `sarex`, `pm`, `sarexApi`, `eavV1`, `gateway`; остальные сервисы объявлены в hosts, но REST-вызовов к ним в этом модуле нет. Кроме REST есть WebSocket (`src/store/stores/gantt/ganttWebsocket.ts`): `io(`${url}/project`, { path: "/message-hub/socket.io" })`, где `url``https://stage-api.sarex.io` (stage) / `https://api.sarex.io` (prod).
## Эндпоинты по модулям
### `store/api/api.ts` — ProjectsAPI (сервис `sarex`, если не указано иное)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getFolderById` | GET | `/api/pm/msp/folders/{folderId}` | Папка по id |
| `getProjectsAndFolders` | GET | `/api/pm/msp/projects/?{query}` | Список проектов и папок |
| `getProjectTemplates` | GET | `/api/pm/msp/projects/?{query}` | Список шаблонов проектов |
| `getAllProjects` | GET | `/api/pm/msp/projects/` | Все проекты |
| `getAllProjectsWithNotFolders` | GET | `/api/pm/msp/projects/?schema=tiny&is_folder=false&company_id={companyId}` | Проекты (без папок) по компании |
| `getProject` | GET | `/api/pm/msp/projects/{projectId}/?extend=true` | Проект (расширенный) |
| `getKeyMilestones` | GET | `/api/pm/msp/projects/{projectId}/key_milestones/` | Ключевые вехи проекта |
| `getResourcePlanning` | GET | `/api/pm/msp/resources/resource_planning/?projects={projects}&start={start}&end={end}&resource_type=human&scale={scale}` | Ресурсное планирование |
| `changeResourceForTask` | PATCH | `/api/pm/msp/resources-tasks/assign/` | Назначить ресурс на задачу |
| `getIntegratedProjects` | GET | `/api/v1/projects/{projectId}/integrated-tasks/` (сервис `pm`) | Интегрированные задачи проекта |
| `createProject` | POST | `/api/pm/msp/projects/` | Создать проект |
| `importProject` / `importProjectV2` | POST | `/api/pm/msp/projects/import/` | Импорт проекта |
| `updateProjectV2` | PATCH | `/api/pm/msp/projects/{projectId}/` | Обновить проект |
| `updateFolders` | GET | `/api/pm/msp/projects/?{idsQuery}` | Папки по id |
| `deleteProject` | DELETE | `/api/pm/msp/projects/{id}` | Удалить проект |
| `createProjectTemplate` | POST | `/api/pm/msp/projects/{projectId}/create_template/` | Создать шаблон из проекта |
| `getProjectStates` | GET | `/api/pm/msp/projects/{id}/states/` | Базовые планы проекта |
| `createProjectState` | POST | `/api/pm/msp/project-states/` | Создать базовый план |
| `updateProjectState` | PUT | `/api/pm/msp/project-states/{id}/` | Обновить базовый план |
| `deleteProjectState` | DELETE | `/api/pm/msp/project-states/{id}/` | Удалить базовый план |
| `patchProjectStateDifferenceData` | PATCH | `/api/pm/msp/project-states/{stateId}/edit_state/` | Изменить данные базового плана |
| `getProjectState` | GET | `/api/pm/msp/project-states/{id}/` | Базовый план по id |
| `getProjectStateData` | GET | `/api/pm/msp/project-states/{id}/data/` | Данные базового плана |
| `addTasksToBasicPlan` | POST | `/api/pm/msp/project-states/{planId}/data/` | Добавить задачи в базовый план |
| `getStatusImport` | GET | `/api/pm/msp/external-task-info/?task_id={uuid}` | Статус фоновой задачи импорта |
| `getTasks` | GET | `/api/pm/msp/projects/{id}/tasks/` | Задачи проекта |
| `taskIndex` | PATCH | `/api/pm/msp/tasks/task_index/` | Переиндексация задач |
| `bulkCreateTasks` | POST | `/api/pm/msp/projects/{project}/create_tasks/` | Массовое создание задач |
| `bulkUpdateTasks` | PATCH | `/api/pm/msp/projects/{project}/update_tasks/` | Массовое обновление задач |
| `bulkDeleteTasks` | DELETE | `/api/pm/msp/projects/{project}/delete_tasks/` | Массовое удаление задач |
| `copyPasteTasks` | POST | `/api/pm/msp/tasks/copy/` | Копирование задач |
| `getTaskDescription` | GET | `/api/pm/msp/tasks/{taskId}/descriptions/` | Описание задачи |
| `updateTaskDescription` | PATCH | `/api/pm/msp/tasks/{taskId}/descriptions/` | Обновить описание задачи |
| `createComment` | POST | `/api/v1/comments/` (сервис `pm`) | Создать комментарий к задаче |
| `getActualValues` | GET | `/api/pm/msp/values/?task={task}` | Фактические значения по задаче |
| `createActualValue` | POST | `/api/pm/msp/values/` | Создать фактическое значение |
| `updateActualValue` | PUT | `/api/pm/msp/values/{id}/` | Обновить фактическое значение |
| `deleteActualValues` | DELETE | `/api/pm/msp/values/{id}/` | Удалить фактическое значение |
| `getAllGanttLinks` | GET | `/api/pm/msp/task-relations/{params}` | Связи задач (Ганта) |
| `createGanttLinks` | POST | `/api/pm/msp/task-relations/` | Создать связи задач |
| `updateGanttLink` | PUT | `/api/pm/msp/task-relations/{id}/` | Обновить связь задач |
| `bulkDeleteRelation` | DELETE | `/api/pm/msp/task-relations/bulk_delete/` | Массовое удаление связей |
| `getResourcesTable` | GET | `/api/pm/msp/resources/?{query}` | Таблица ресурсов |
| `updateResources` | PUT | `/api/pm/msp/resources/{id}/` | Обновить ресурс |
| `deleteResource` | DELETE | `/api/pm/msp/resources/{id}/` | Удалить ресурс |
| `createVisualProfile` | POST | `/api/pm/msp/visual-profiles/` | Создать визуальный профиль |
| `editVisualProfile` | PATCH | `/api/pm/msp/visual-profiles/{id}` | Изменить визуальный профиль |
| `getVisualProfiles` | GET | `/api/pm/msp/projects/{projectId}/profiles/` | Визуальные профили проекта |
| `getMyTasks` | GET | `/api/pm/msp/tasks/?responsible=true&executors=true&{query}` | Мои задачи |
| `copyProject` | POST | `/api/pm/msp/projects/{projectId}/copy_project/` | Копировать проект |
| `createProjectDocument` | POST | `/api/pm/msp/projects/{projectId}/ksg_docs_sync/` | Синхронизация КСГ-документов |
| `getUsersByCompanyId` | GET | `/api/core/v2/users/?company={companyId}&{query}` | Пользователи компании |
| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={companyId}` | Отделы компании |
| `getPositionsV2` | GET | `/api/core/admin/positions/?company={companyId}` | Должности компании |
| `getDocumentStatus` | GET | `/flows/api/v1/documents/?full=true&document_ids={ids}` (сервис `sarexApi`) | Статус документов |
| `exportTasksPDF` | POST | `/api/pm/msp/projects/{id}/export_project_to_pdf/` | Экспорт проекта в PDF (+опрос `external-task-info`) |
| `projectExport` | POST | `/api/pm/msp/projects/{id}/project_export/` | Экспорт проекта (xlsx/xml) |
### `store/api/attributes-api-v2.ts` — AttributesApiV2
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getProjectAttributes` | GET | `/api/pm/msp/project-attribute/?project={projectId}` (сервис `sarex`) | Атрибуты проекта |
| `updateProjectAttribute` | PUT | `/api/pm/msp/project-attribute/{id}/` (сервис `sarex`) | Обновить атрибут проекта |
| `deleteProjectAttribute` | DELETE | `/api/pm/msp/project-attribute/{id}/` (сервис `sarex`) | Удалить атрибут проекта |
| `addAttributesToProject` | POST | `/api/pm/msp/project-attribute/` (сервис `sarex`) | Привязать атрибуты к проекту |
| `getTimeMarkersData` | GET | `/api/pm/msp/projects/{projectID}/time_markers_data/?attributes={ids}&with_hierarchy={flag}` (сервис `sarex`) | Данные временных маркеров |
| `getAttributesList` | GET | `/api/v4/attribute/?model_name=gantt-task&company_id={companyId}` (сервис `eavV1`) | Список атрибутов (EAV) |
| `getAssetsByAttribute` | GET | `/api/v4/assets/?path_contains={assetsId}` (сервис `eavV1`) | Ассеты по атрибуту |
| `getAttributesByAssetsParent` | GET | `/api/v4/assets/?tenant_id={companyId}&depth=0` (сервис `eavV1`) | Корневые ассеты компании |
| `createNewAttribute` | POST | `/api/v2/attribute/` (сервис `eavV1`) | Создать атрибут (EAV) |
| `updateAttribute` | PATCH | `/api/v2/attribute/{id}/` (сервис `eavV1`) | Обновить атрибут (EAV) |
### `store/api/calculate-api.ts` — CalculatesAPI (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `postFormula` | POST | `/api/pm/msp/projects/{id}/formula/` | Пересчёт по формуле |
### `store/api/calendarsApi.ts` — CalendarsApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getAllCalendars` | GET | `/api/pm/msp/calendars/` | Все календари |
| `getCalendarById` | GET | `/api/pm/msp/calendars/{id}/` | Календарь по id |
| `createCalendar` | POST | `/api/pm/msp/calendars/` | Создать календарь |
| `editCalendar` | PATCH | `/api/pm/msp/calendars/{id}/` | Изменить календарь |
| `deleteCalendar` | DELETE | `/api/pm/msp/calendars/{id}/` | Удалить календарь |
### `store/api/issuesApi.ts` — IssuesApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getIssueTypes` | GET | `/api/pm/msp/entity-relations/?project_id={projectId}` | Типы связей/проблем проекта |
| `patchIssueType` | PATCH | `/api/pm/msp/entity-relations/{issueTypeId}/` | Обновить тип связи |
| `getIssueData` | GET | `/api/pm/msp/projects/{projectId}/issues_data/` | Данные проблем проекта |
### `store/api/ksgStatesApi.ts` — KsgStatesApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getProjestStates` | GET | `/api/pm/msp/project-settings/?{query}` | Настройки/состояния КСГ |
| `createProjectState` | POST | `/api/pm/msp/project-settings/` | Создать состояние КСГ |
| `editProjectState` | PATCH | `/api/pm/msp/project-settings/{id}/` | Изменить состояние КСГ |
| `deleteProjectAttribute` | DELETE | `/api/pm/msp/project-settings/{id}/` | Удалить состояние КСГ |
| `checkApplyState` | POST | `/api/pm/msp/project-settings/{id}/apply/` | Применить состояние КСГ |
### `store/api/permissionsApi.ts` — PermissionsApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getPermissions` | GET | `/api/pm/msp/projects/{projectId}/permissions/` | Права проекта |
| `getTaskPermissions` | GET | `/api/pm/msp/tasks/{taskId}/permissions/` | Права задачи |
| `getAllTaskPermissions` | GET | `/api/pm/msp/projects/{projectId}/all_permissions/` | Все права задач проекта |
| `savePermission` | POST | `/api/pm/msp/projects/{projectId}/permissions/` | Сохранить права проекта |
| `saveTaskPermission` | POST | `/api/pm/msp/tasks/{taskId}/permissions/` | Сохранить права задачи |
### `store/api/relationApi.ts` — RelationsApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getProjectRelations` | GET | `/api/pm/msp/projects/{projectId}/relations/` | Связи/интеграции проекта |
| `postProjectIntegrations` | POST | `/api/pm/msp/projects/{id}/bulk_integration/` | Массовое создание интеграций |
| `updateProjectIntegration` | PATCH | `/api/pm/msp/project-relations/{id}/` | Обновить интеграцию |
| `deleteProjectIntegration` | DELETE | `/api/pm/msp/project-relations/{id}/` | Удалить интеграцию |
### `store/api/reviewsApi.ts` — ReviewAPI (сервис `sarexApi`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `createReview` | POST | `/flows/api/v1/reviews/` | Создать ревью/согласование |
| `getProcesses` | GET | `/flows/api/v1/flows/?{query}` | Процессы/потоки согласования |
### `store/api/systemLogApi.ts` — SystemLogApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getNewSystemLogs` | GET | `/api/pm/msp/projects/{projectId}/system-logs/?{query}` | Системные логи проекта |
| `getDetailsSystemLog` | GET | `/api/pm/msp/projects/{projectId}/system-log-detail/?log_id={logId}` | Детали записи лога |
| `rollBack` | POST | `/api/pm/msp/projects/{projectId}/rollback-to-record/` | Откат к записи лога |
### `store/api/taskDetailingApi.ts` — TaskDetailingApi (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getRole` | GET | `/api/pm/msp/projects/{projectId}/rule/` | Правило детализации проекта |
| `createRule` | POST | `/api/pm/msp/projects/{projectId}/rule/` | Создать правило детализации |
| `getDetailTasks` | GET | `/api/pm/msp/detailed-tasks/?task={taskId}` | Детализированные задачи |
| `patchDetailTasks` | PATCH | `/api/pm/msp/detailed-tasks/{detailingTaskId}/` | Обновить детализированную задачу |
### `store/api/workspaceApi.ts` — WorkspaceAPI (сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getResourcesByTaskId` | GET | `/api/pm/msp/resources-tasks/?tasks={task}` | Ресурсы по задаче |
| `getResourcesByIds` | GET | `/api/pm/msp/resources/?ids={resourcesIds}` | Ресурсы по id |
| `loadElementsByResourceIds` | GET | `/api/pm/msp/resources-elements/?resources={ids}` | Элементы ресурсов |
| `connectResourcesTasks` | POST | `/api/pm/msp/resources-tasks/` | Привязать ресурс к задаче |
| `editResourcesTasks` | PATCH | `/api/pm/msp/resources-tasks/bulk_update/` | Массово изменить связи ресурс-задача |
| `deleteResource` | DELETE | `/api/pm/msp/resources-tasks/bulk_delete/` | Массово удалить связи ресурс-задача |
### `services/api/fetch/gateway.ts` — GatewayAPI (сервис `gateway`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchResources` | GET | `/api/v1/resources` | Ресурсы шлюза (доступы/фичи) |
### Gantt-репозитории (`src/pages/TasksNew/GanttWorkspace/repositories/*`, сервис `sarex`)
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `WorkspaceSelectedKSGProjectRepository` | GET | `/api/pm/msp/projects/?bundle_id={bundleID}&schema=tiny` | Проекты выбранного бандла КСГ |
| `TaskResourceConnectionBaseRepository` | GET | `/api/pm/msp/projects/{id}/profiles/` | Профили ресурсов проекта |
| `TaskResourcesConnectionsRepository` | GET | `/api/pm/msp/resources-tasks/?projects={project}` | Связи ресурс-задача по проекту |
| `TasksRepository` (список) | GET | `/api/pm/msp/tasks/?project={project}&{query}` | Задачи проекта |
| `TasksRepository` (одна) | GET | `/api/pm/msp/tasks/{id}/` | Одна задача |
| `uploadFileToServer` | POST (multipart) | `/api/pm/msp/descriptions/upload_file/` | Загрузка файла/изображения в описание |
## Обработка ошибок
Централизованного middleware (RTK Query `createApi`/`fetchBaseQuery` не используется) нет — API-модули оборачивают axios-based `httpService`. Типичные паттерны: большинство вызовов `.then(r => r.data)` и пробрасывают ошибку выше; часть — `try/catch` с `isAxiosError(error)`, где `403` даёт «Нет доступа»/«Доступ запрещён», а прочие ошибки — общее сообщение (напр. «Не удалось сохранить данные, попробуйте ещё раз»), нередко показываемое через `createToast(...)` и повторно выбрасываемое как `new Error(...)`. Некоторые читают `error.response.data.detail`. `getStatusImport` при ошибке возвращает `{ request_failed: true }`; `exportTasksPDF` опрашивает `external-task-info` каждые 2 с до `is_ready`. Часть вызовов передаёт в SDK опцию `errorMessage` (напр. «Некорректные данные»).

1768
apps/pm/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
# prescriptions-frontend — переменные СБОРКИ и CI
#
# ВАЖНО: у фронтенда нет рантайм-.env. Собранный бандл — статика, которую
# раздаёт nginx. Все переменные ниже используются на этапе СБОРКИ образа
# (Dockerfile ARG / --build-arg в .gitlab-ci.yml) и локального запуска.
# Значение BUILD_ENV валидируется в env.js и внедряется в бандл через
# webpack DefinePlugin. Подробности — в CONFIGURATION.md.
# Build (обязательно). Одно из: local | stage | preprod | prod | contour
BUILD_ENV=stage
# Флаг сборки под Storybook: "true" | "false"
STORYBOOK=false
# Токен приватного npm-реестра nexus.infra.sarex.io (см. .npmrc)
NPM_TOKEN=
# --- Cypress e2e ---
# Задаются в cypress.env.json (пример — cypress.env-example.json), не в .env:
# SRX_LOGIN=
# SRX_PASSWORD=

View File

@ -0,0 +1,127 @@
# Конфигурация проекта prescriptions-frontend
Документ описывает способы конфигурирования микрофронтенда `prescriptions-frontend` (Module Federation `srx_prescriptions`, экспонирует `./PrescriptionsPage`), его переменные сборки/CI и параметры деплоя.
## Способы конфигурирования
В отличие от backend-сервисов, у фронтенда **нет рантайм-конфигурации через `.env`**: собранный бандл — статические файлы, которые раздаёт nginx. Всё поведение задаётся **на этапе сборки** одной переменной — `BUILD_ENV`.
- `env.js` читает `process.env.BUILD_ENV` и валидирует его против списка `local`/`stage`/`prod`/`contour`/`preprod` (иначе — ошибка сборки `set BUILD_ENV one of ...`);
- `webpack.config.js` через `DefinePlugin` внедряет глобальные константы `BUILD_ENV` и `STORYBOOK` в бандл;
- `build.config.js` по `BUILD_ENV` выбирает режим сборки (`mode`/`devtool`);
- `module/api/hosts.ts` и `module/api/module-hosts.ts` содержат карты хостов по окружениям; SDK (`@sarex-team/sdk-js`) на рантайме выбирает нужный хост по глобальной константе `BUILD_ENV` (по умолчанию `prod`).
Отдельного конфиг-файла (yaml/toml) у приложения нет.
### Значения `BUILD_ENV`
| `BUILD_ENV` | `mode` | `devtool` | Назначение |
| --- | --- | --- | --- |
| `local` | `development` | `eval-source-map` | Локальная разработка (dev-server, storybook), `httpService``original` |
| `stage` | `development` | `eval-source-map` | Стенд stage |
| `preprod` | `production` | `source-map` | Предпрод |
| `prod` | `production` | `source-map` | Прод |
| `contour` | `production` | `source-map` | Изолированный контур (относительные пути хостов) |
## Переменные сборки и CI
| Переменная | Где используется | Назначение |
| --- | --- | --- |
| `BUILD_ENV` | `env.js`, `webpack.config.js`, `Dockerfile` (ARG), `.gitlab-ci.yml` (`--build-arg`) | Целевое окружение сборки. Обязательна |
| `NPM_TOKEN` | `.npmrc`, `Dockerfile` (ARG), `.gitlab-ci.yml` (`--build-arg`) | Токен доступа к приватному npm-реестру `nexus.infra.sarex.io` |
| `STORYBOOK` | `webpack.config.js` (`DefinePlugin`), `module/pages/index.tsx` | Флаг сборки под Storybook (`"true"`/`"false"`) |
Cypress-тесты берут учётные данные из `cypress.env.json` (пример — `cypress.env-example.json`): `SRX_LOGIN`, `SRX_PASSWORD`.
Версия Node для разработки — `v20.16.0` (`.nvmrc`).
## Хосты по окружениям
Базовые API-хосты подставляются из `module/api/hosts.ts` по `BUILD_ENV` (детальная разбивка по сервисам — в `ENDPOINTS.md`):
| Окружение | Базовый API | Пример (`documentations`) |
| --- | --- | --- |
| `local` / `stage` | `https://stage-api.sarex.io` | `https://stage-api.sarex.io/documentations/api/v1` |
| `preprod` | `https://api.preprod.sarex.io` | `https://api.preprod.sarex.io/documentations/api/v1` |
| `prod` | `https://api.sarex.io` | `https://api.sarex.io/documentations/api/v1` |
| `contour` | относительные пути | `/documentations/api/v1` |
Удалённый модуль (Module Federation) `documentations` подключается по `module/api/module-hosts.ts` (`remoteEntry.js`).
## Запуск и скрипты (`package.json`)
| Команда | Назначение |
| --- | --- |
| `npm run serve-module` | Dev-server (`webpack.dev.js`, порт `9001`, https, proxy `/api`, `/admin` на `appUrl`), `BUILD_ENV=local` |
| `npm run storybook` | Storybook (порт `9000`, https), `BUILD_ENV=local` |
| `npm run start` / `test:dev` | Параллельный запуск serve-module + storybook (+ cypress в `test:dev`) |
| `npm run build-module` | Продакшн-сборка модуля (`webpack --config webpack.config.js`) |
| `npm run build-storybook` | Сборка статики Storybook |
| `npm run cypress:open` | Запуск e2e-тестов Cypress |
| `npm run lint` / `lint:ts` | Prettier / проверка типов `tsc --noEmit` |
## Сборка образа (`Dockerfile`)
Двухстадийная сборка:
1. `node:15` — установка зависимостей (`npm i --legacy-peer-deps` с `NPM_TOKEN`), `npm run lint`, `BUILD_ENV=$BUILD_ENV npm run build-module``/app/dist`;
2. `nginx:mainline-alpine-otel` — копирование `dist` в `/dist` и конфига `nginx/nginx.conf`.
Build-args: `BUILD_ENV`, `NPM_TOKEN`.
nginx (`nginx/nginx.conf`) раздаёт статику из `/dist`, отдаёт `/ping``{"result": "ok"}` (healthcheck) и запрещает кеширование `/module/remoteEntry.js` (`Cache-Control: no-store`).
## CI/CD (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). `SERVICE_NAME=prescriptions-frontend`. Окружение переключается по ветке/тегу (`workflow.rules`):
| Условие | STAND | NAMESPACE | BUILD_ENV | CHART_VERSION |
| --- | --- | --- | --- | --- |
| ветка `stage` | `stage` | `proc` | `stage` | `1.0.0-stage` |
| ветка `master` | `preprod` | `prescriptions-preprod` | `preprod` | `1.0.0-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `prescriptions-prod` | `prod` | `1.0.0-prod` |
| merge request | — | — | — | сборка образа отключена (`ENABLE_BUILD_IMAGE=false`) |
Деплой параметризуется через `HELM_SET_ARGS` (образ, `global.env`, `commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`).
## Helm-чарт проекта (`.helm`)
`Chart.yaml`: зависимость `universal-chart` (`oci://cr.yandex/.../charts`, версия `0.1.7`).
`values.yaml` (`universal-chart.services.frontend`):
- `deployment.name = prescription-frontend`, `replicaCount = 1`, `port = 80`, `revisionHistoryLimit = 10` (prod — `15`);
- `image.name = cr.yandex/crp3ccidau046kdj8g9q/prescription-frontend`, `pullPolicy = IfNotPresent`;
- `service.name = prescriptions-frontend-service`, `type = ClusterIP`, `port/targetPort = 80`;
- `imagePullSecrets = dockerhub`;
- probes (`liveness`/`readiness`) на `/ping:80` заданы, но **выключены** (`enabled: false`);
- `envs: []`, `secretEnvs: []` — переменных окружения контейнеру не передаётся;
- ресурсы: `requests` `memory 100Mi`, `cpu 100m`.
## Инфраструктура (`iac/apps/prescriptions`)
Разворачивается через Kustomize. Состав каталога:
| Путь | Назначение |
| --- | --- |
| `base/namespace.yaml` | Namespace `prescriptions` с `istio-injection: enabled` |
| `base/deployment.yaml` | Deployment `frontend`, образ `cr.yandex/.../prescriptions-frontend:production_...`, порт `80`, `requests` `cpu 25m`/`memory 100Mi`, `imagePullSecrets: regcred` |
| `base/service.yaml` | Service `frontend-service`, `ClusterIP`, порт `80` |
| `base/kustomization.yaml` | Сборка base (namespace + deployment + service) |
| `yc-k8s-test/` | Оверлей поверх `../base` (патч `replicas.yaml` закомментирован) |
| `brusnika-prod/`, `brusnika-stage/` | Оверлеи (см. замечание ниже) |
## Замечания и потенциальные проблемы
- **Оверлеи `brusnika-prod` / `brusnika-stage`** сейчас содержат `HelmRelease` бэкенда `measurements` (namespace `measurements`, образ `documentations`), не относящийся к prescriptions — похоже на копипаст-заготовку, которую нужно заменить на конфигурацию prescriptions-frontend либо удалить.
- **Два способа описания деплоя**: helm-чарт в репозитории фронтенда (`.helm`, `universal-chart`) и Kustomize-манифесты в инфраструктуре (`iac/apps/prescriptions`) описывают один и тот же сервис по-разному — стоит зафиксировать единый источник истины.
- **Несогласованные имена**: `SERVICE_NAME=prescriptions-frontend` (мн. ч.), а `deployment.name`/`image.name` в `.helm``prescription-frontend` (ед. ч.). В инфра-манифестах deployment называется просто `frontend`.
- **Версии Node расходятся**: разработка — `v20.16.0` (`.nvmrc`), сборка образа — `node:15` (`Dockerfile`).
- **Мёртвая конфигурация**: экспорт `hosts` в `networking.config.js` (внедрение `__<service>_host`) и `module/Env` (`IEnv`, `process.env as IEnv`) в коде модуля не используются; `dotenv-webpack` присутствует в devDependencies, но не подключён в `webpack.config.js`. `networking.config.js` реально используется только в `webpack.dev.js` (прокси dev-сервера).
## Минимальный набор для сборки
- `BUILD_ENV` — одно из `local`/`stage`/`preprod`/`prod`/`contour` (обязательно);
- `NPM_TOKEN` — для установки приватных пакетов `@sarex-team/*` из nexus;
- (опционально) `STORYBOOK=true` — при сборке/запуске Storybook;
- (для e2e) `SRX_LOGIN`, `SRX_PASSWORD` в `cypress.env.json`.

View File

@ -0,0 +1,166 @@
# Эндпоинты, с которыми взаимодействует prescriptions-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `prescriptions-frontend`, Module Federation `srx_prescriptions`, экспонирует `./PrescriptionsPage`).
## Как устроено взаимодействие
Запросы выполняются через единый `httpService` (`module/api/http-service.ts`), созданный фабрикой `createHttpService` из [`@sarex-team/sdk-js`](https://www.npmjs.com/) поверх `axios`. Каждый вызов задаётся объектом с полями:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (дописывается к базовому хосту сервиса);
- `data` — тело запроса (для `POST`/`PUT`/`PATCH`);
- `queryKey`, `axiosConfig` (в т.ч. `responseType: "blob"` для файлов), `params` — опции кеширования/повторов и параметры запроса.
Метод HTTP определяется вызываемой функцией `httpService`: `getRequest`/`postRequest`/`putRequest`/`patchRequest`/`deleteRequest`.
Базовый хост подставляется SDK по паре (`BUILD_ENV`, `service`) из реестра `module/api/hosts.ts`. Значение `BUILD_ENV` задаётся на этапе сборки (`webpack.config.js` → `DefinePlugin`, глобальная константа `BUILD_ENV`), по умолчанию — `prod` (`http-service.ts`: `BUILD_ENV ?? "prod"`). В окружении `local` тип сервиса переключается на `original` (`setTypeOfHttpService("original")`).
Итоговый URL = `<базовый хост сервиса>` + `url`.
Определения вызовов сосредоточены в `module/api/*` (`index.ts`, `contractsApi.ts`, `resourcesApi.ts`, `templatesApi.ts`, `marks.ts`) и частично в сторах (`module/store/stores/resources.ts`, `module/store/stores/users.ts`).
## Базовые хосты по сервисам и окружениям
Значения из `module/api/hosts.ts`. Помимо `stage`/`prod` определены окружения `local`, `preprod` и `contour``contour` — относительные пути для изолированного контура).
| Сервис (`service`) | Назначение | `stage` | `prod` |
| --- | --- | --- | --- |
| `prescriptions` | Предписания (поверх issues) | `https://stage-api.sarex.io/issues/api/prescriptions` | `https://api.sarex.io/issues/api/prescriptions` |
| `issues` | Сервис замечаний/issues | `https://stage-api.sarex.io/issues/api` | `https://api.sarex.io/issues/api` |
| `documentations` | Сервис документации (документы, бандлы, диски) | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` |
| `gateway_api_v1` | Gateway API v1 (ресурсы, документы, шаблоны) | `https://stage-api.sarex.io/gateway/api/v1` | `https://api.sarex.io/gateway/api/v1` |
| `gateway_api_v2` | Gateway API v2 (пользователи, ресурсы) | `https://stage-api.sarex.io/gateway/api/v2` | `https://api.sarex.io/gateway/api/v2` |
| `sarex` | Основной backend (core/client) | `https://stage.sarex.io` | `https://lk.sarex.io` |
| `sarexApi` | API Sarex (contracts) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `eav_api_v0` | Сервис атрибутов (EAV) | `https://stage-api.sarex.io/eav/api/v0` | `https://api.sarex.io/eav/api/v0` |
| `orchestrator` | Оркестратор процессов (маркировка, подпись) | `https://stage-api.sarex.io/orchestrator` | `https://api.sarex.io/orchestrator/api` |
| `files` | Сервис файлов (скачивание) | `https://stage-api.sarex.io/files/api/v1` | `https://api.sarex.io/files/api/v1` |
| `lambdas` | Лямбды (экспорт reviews) | `https://stage-api.sarex.io/lambdas` | `https://api.sarex.io/lambdas` |
| `checklists` | Сервис чек-листов | `https://stage-api.sarex.io/checklists/api/v1` | `https://api.sarex.io/checklists/api/v1` |
| `zitadel` | IdP (аутентификация) | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Подключаемый удалённый модуль (Module Federation) описан отдельно в `module/api/module-hosts.ts`: `documentations``remoteEntry.js` (stage: `https://stage-modules.sarex.io/documentations/static/module/remoteEntry.js`, prod: `https://modules.sarex.io/documentations/static/module/remoteEntry.js`). Хост выбирается функцией `getModuleHost(moduleName)` по `BUILD_ENV`.
## Эндпоинты по сервисам
### `prescriptions` — Предписания
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getPrescriptions` | GET | `/?{query}` | Список предписаний (фильтры/поиск, сериализация в `serializePrescriptionParams`) |
| `createPrescription` | POST | `/prescription/` | Создать предписание ⚠ вызывается с `service: "prescription"` (см. замечания) |
| `getPrescriptionById` | GET | `/{id}/` | Предписание по id |
| `editPrescription` | PATCH | `/{id}/` | Редактировать предписание |
| `deletePrescription` | DELETE | `/{id}/` | Удалить предписание |
| `exportPrescriptionById` | GET | `/{id}/export/?file_format={docx\|pdf}` | Экспорт предписания в docx/pdf |
| `getStatusCount` | GET | `/status-count/?{query}` | Счётчики по статусам |
| `getHistoryByCompanyId` | GET | `/history/?company_id={id}` | История предписаний компании |
| `getHistoryByPrescriptionId` | GET | `/{id}/history/` | История конкретного предписания |
### `issues` — Замечания / статусы
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getStatusModels` | GET | `/prescription-status-models/?company_id={id}` | Модели статусов предписаний |
| `getCompanyStatuses` | GET | `/prescription-statuses/?company_id={id}` | Статусы предписаний компании |
| `getIssues` | GET | `/issues/?{params}` | Список замечаний |
| `getCustomStatuses` | GET | `/companies/{companyId}/status-model/v2/` | Кастомная модель статусов компании |
### `documentations` — Сервис документации
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getDocument` | GET | `/documents/{id}` | Документ по id |
| `getDisks` | GET | `/disks` | Список дисков (используется в `DocumentAPI` и `TemplatesApi`) |
| `mark` | PUT | `/bundles/{bundleId}/marks` | Добавить штампы/QR/подписи в бандл |
| `sign` | POST | `/bundles/{bundleId}/sign` | Подписать бандл |
| `downloadFile` | GET | `/bundles/{bundleId}/{key}/download` | Скачать файл бандла (`responseType: blob`) |
### `gateway_api_v1` — Gateway API v1
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getResources` (store) | GET | `/resources/?company_id={id}` | Список ресурсов компании |
| `fetchParentDocumentByResourceId` | GET | `/resources-rpc/parent-document-by-resource-id/{resourceId}/` | Родительский документ по resource id |
| `fetchDocumentsBundleVersions` | POST | `/documents/bundle_versions` | Версии бандлов документов |
| `getDocumentAncestors` | POST | `/documents/ancestors` | Предки документов |
| `getTemplates` | GET | `/disks/{diskId}/flat_documents/?type={type}` | Шаблоны диска (плоский список) |
### `gateway_api_v2` — Gateway API v2
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getUsersByResourceId` | GET | `/users/?{query}` | Пользователи по фильтру ресурса |
| `getResourceFullInfo` | GET | `/resources/{resourceId}/` | Полная информация о ресурсе |
### `sarex` — Основной backend (core/client)
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getUsersByCompanyId` | GET | `/api/core/users/?company={id}&{query}` | Пользователи компании |
| `getDepartments` | GET | `/api/core/admin/departments/?company={id}` | Отделы компании |
| `getDepartmentsV2` | GET | `/api/core/admin/departments/?company={id}&{query}` | Отделы компании (с доп. query) |
| `getPositions` | GET | `/api/core/admin/positions/?company={id}` | Должности компании |
| `getPositionsV2` | GET | `/api/core/admin/positions/?company={id}&{query}` | Должности компании (с доп. query) |
| `getSettings` (store) | GET | `/api/client/settings/` | Клиентские настройки |
### `sarexApi` — API Sarex (contracts)
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getContracts` | GET | `/contracts/api/v0/contracts/?tenant_id={companyId}` | Договоры компании |
| `getContractsByContractorId` | GET | `/contracts/api/v0/contracts/?tenant_id={companyId}&contractor_id={id}` | Договоры по контрагенту |
### `eav_api_v0` — Сервис атрибутов (EAV)
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getAttributes` | GET | `/attribute/?company_id={id}` | Атрибуты компании |
### `orchestrator` — Оркестратор процессов
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `createMarkFlow` | POST | `/process` | Запустить процесс маркировки |
| `getMarkFlow` | GET | `/process/{id}` | Процесс по id |
| `startSign` | POST | `/sign` | Запустить подписание |
### `files` — Сервис файлов
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `downloadDocuments` | POST | `/documents/` | Скачать документы по `bundle_ids` (`responseType: blob`) |
### `lambdas` — Лямбды (экспорт)
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchExportReviewsByResourceIDs` | GET | `/export-reviews/{params}` | Экспорт reviews (xlsx) |
| `fetchExportReview` | GET | `/export-reviews/{reviewId}/report/` | Отчёт по review (pdf) |
### `flows` — Процессы (⚠ сервис не задан в hosts.ts)
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `changeCopyPaths` | PATCH | `/documents/change-copy-paths/` | Изменить пути копий документов |
## Обработка ошибок
Отдельного модуля-маппера ошибок (`errors.ts`) в проекте нет — обработка распределена:
- часть обёрток (`ContractsApi`, `ResourcesApi`, `TemplatesApi`) при ошибке пробрасывают `throw new Error(error)`;
- часть функций (`fetchParentDocumentByResourceId`, `fetchExportReview*`) гасят ошибку через `console.error` и не пробрасывают её;
- тип ответа об ошибке — `ErrorResponse` (`module/api/types.ts`): читается `response.data.detail`;
- статусы запроса в сторах: `RequestStatus``init`/`loading`/`success`/`fetching`/`error`/`permissionError`.
## Права доступа (`module/api/permissions.ts`)
Модуль оперирует правами `core.*`: `can_view_prescription`, `can_add_prescription`, `can_edit_prescription`, `can_delete_prescription`, `can_view_all_prescriptions`, `can_admin_prescription`. Группы (`FG_PERMISSIONS`): `ADMIN` (все права), `AUTHOR` (просмотр + создание), `RESPONSIBLE` и `VIEW_ALL` (просмотр).
## Замечания и потенциальные проблемы
- **`prescription` (единственное число)** — `createPrescription` вызывается с `service: "prescription"`, но такого ключа в `module/api/hosts.ts` нет (есть только `prescriptions`). Базовый хост не резолвится корректно — вероятно опечатка, следует использовать `prescriptions`.
- **`flows`** — `changeCopyPaths` использует `service: "flows"`, который также не задан в `hosts.ts`. Ключ нужно добавить в реестр либо исправить.
- **`contour`** — в окружении `contour` не определён сервис `sarexApi`, поэтому `getContracts`/`getContractsByContractorId` в этом контуре работать не будут.
- **`orchestrator`** — в `prod` базовый URL с суффиксом `/api` (`.../orchestrator/api`), а в `stage`/`local`/`preprod` — без него. Пути эндпоинтов (`/process`, `/sign`) следует проверять с учётом этого различия.
- **`checklists` и `zitadel`** заданы в `hosts.ts`, но напрямую через `httpService` в модуле не вызываются (`zitadel` — IdP, используется SDK для авторизации; `checklists` в текущем коде модуля не используется).

View File

@ -0,0 +1,246 @@
# Конфигурация проекта workflows-api
`workflows-api` — HTTP-сервис (Go 1.24, фреймворк [Fiber v2](https://github.com/gofiber/fiber)) для работы с workflow: создание, чтение, перезапуск задач, отмена запусков, приоритизация. Хранилище — PostgreSQL. Трейсинг — OpenTelemetry (через внешнюю библиотеку `gitlab.sarex.io/infra/golang-fiber-otel-tools`).
## Способы конфигурирования
Конфигурация читается **только из переменных окружения**. Используется библиотека [`github.com/ilyakaznacheev/cleanenv`](https://github.com/ilyakaznacheev/cleanenv) (`cleanenv.ReadEnv`). Файлы конфигурации (`.yaml`, `.json`) не читаются — вызывается именно `ReadEnv`, а не `ReadConfig`.
- **Префикс** у переменных отсутствует — используются «плоские» имена (`POSTGRES_ADDRESS`, `HTTP_HOST` и т. п.).
- **Вложенность** структуры `Config` описывается через встроенные (embedded) структуры (`App`, `Log`, `HTTP`, `pgxconnection.Postgres`, `TRACER`, `Execution`), но на имена переменных это не влияет — теги `env` заданы плоско.
- Значения по умолчанию задаются тегом `env-default`.
- Булевы значения cleanenv принимает как `true/false`, а также `1/0` (в Helm используется числовая форма).
Точка сборки конфигурации — `config/config.go`, функция `config.New()`. Отдельно, для запуска миграций, вторая структура `pkg/postgres/gopg.Postgres` читается своим вызовом `cleanenv.ReadEnv` в `gopg.GetPgConnectionWithoutConfig()`**у неё те же имена переменных, но частично другие значения по умолчанию** (см. «Замечания»).
### Способы запуска
| Способ запуска | Откуда берутся переменные |
| --- | --- |
| Бинарь `httpserver` (production, `Dockerfile` `ENTRYPOINT ["/httpserver", "migrate"]`) | Переменные окружения контейнера (в k8s — из Helm-чарта: блоки `envs` и `secretEnvs`) |
| Бинарь `migrations` (отдельный ранер миграций) | Переменные окружения контейнера |
| Локальный запуск через `air` (`.air.toml`, hot-reload, сборка `./cmd/httpserver/main.go`) | Переменные окружения оболочки / `.env` (подхватываются вручную), значения по умолчанию из кода |
| `docker-compose up` (`docker-compose.yaml`) | `environment:` в compose + значения по умолчанию из кода |
### Точки входа и вспомогательные скрипты
| Файл / скрипт | Назначение |
| --- | --- |
| `cmd/httpserver/main.go` | Основная точка входа. Если передан хотя бы один аргумент (например `migrate`) — сначала выполняет миграции (`go-pg-migrations`), затем поднимает Fiber-сервер |
| `cmd/migrations/main.go` | Отдельный бинарь только для миграций (без запуска сервера) |
| `Dockerfile` | Multi-stage сборка: собирает `httpserver` и `migrations`, `ENTRYPOINT ["/httpserver", "migrate"]` |
| `entrypoint.sh` | Скрипт-обёртка (`/go/bin/migrations migrate` → `/go/bin/httpserver`). **Не используется** Dockerfile и ссылается на несуществующие пути бинарей — устаревший артефакт (см. «Замечания») |
| `.air.toml` | Конфиг hot-reload `air` для локальной разработки |
| `docker-compose.yaml` | Локальный стенд: PostgreSQL 14-alpine + сборка API. Блок `migrations` закомментирован |
| `Makefile` | Юнит-тесты, генерация моков, поднятие/сборка контейнера БД (`.docker/postgres`) |
| `.docker/postgres/Dockerfile` | Образ локальной БД для `make container-run-deps` |
### Порядок старта контейнера (production)
1. Контейнер стартует с `ENTRYPOINT ["/httpserver", "migrate"]`.
2. `httpserver` видит аргумент `migrate` (`len(os.Args) > 1`) → открывает подключение к БД через `go-pg` (`gopg.GetPgConnectionWithoutConfig`, читает env заново) и прогоняет миграции из `cmd/migrations/migrationfiles`.
3. После миграций поднимается Fiber-приложение (`server.New` → `server.Run`), подключается пул `pgx` (`pgxconnection.GetPgConnection`), при `TRACER_USE=true` инициализируется трейсер/otel-логгер.
4. Сервер слушает адрес из `HTTP_HOST`. Health-check — `GET /ping`.
## Переменные приложения
Ниже — переменные, которые **реально читает код** (`config/config.go` + `pkg/postgres/pgxconnection/postgres.go`).
### App (`config/config.go`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `APP_NAME` | string | `workflows-api` | Имя приложения (`App.Name`) |
| `APP_VERSION` | string | `v1` | Версия приложения (`App.Version`) |
### Log
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `LOG_LEVEL` | string | `info` | Уровень логирования (`logging.NewLogger`) |
### HTTP
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `HTTP_HOST` | string | `0.0.0.0:8000` | Адрес и порт прослушивания Fiber-сервера |
| `PUBLIC_KEY` | string | — (пусто) | PEM-публичный ключ (PKIX) для проверки JWT Sarex. **Обязателен**: при пустом значении `auth.New` вызывает `panic` на старте |
| `HTTP_BODY_LIMIT` | int | `268435456` (256 MiB) | Максимальный размер тела запроса (`fiber.Config.BodyLimit`) |
| `HTTP_READ_BUFFER_SIZE` | int | `98304` (96 KiB) | Размер буфера чтения (`fiber.Config.ReadBufferSize`) |
### Database (`pkg/postgres/pgxconnection`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | `localhost` | Хост PostgreSQL |
| `POSTGRES_DB` | string | `processing_db` | Имя базы данных |
| `POSTGRES_USER` | string | `sarex` | Пользователь БД |
| `POSTGRES_PASSWORD` | string | `sarex` | Пароль БД |
| `POSTGRES_PORT` | string | `5432` | Порт PostgreSQL |
| `POSTGRES_POOL_SIZE` | int | `3` | Размер пула (используется только для логирования; фактический размер пула pgx задаётся строкой подключения) |
| `ENABLE_SQL_QUERY` | bool | `true` | Флаг логирования SQL. **Читается в конфиг, но нигде не используется** (см. «Замечания») |
| `YC-PG-CERTIFICATE` | string | — (пусто) | CA-сертификат (PEM) для TLS-подключения к БД. Непустое значение включает TLS |
| `POSTGRES_SSL_USE` | bool | `false` | Включение TLS-подключения к БД |
### Tracer (`config.TRACER`, OpenTelemetry)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `TRACER_USE` | bool | `false` | Включить трейсинг/otel-логгер и otelfiber-middleware |
| `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора (gRPC) |
| `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору |
| `SERVICE_NAME` | string | `wf-test-db` | Имя сервиса в трейсах |
| `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя otel-логгера |
### Execution (лимиты ресурсов задач, `config.Execution`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `MAX_CPU_REQUESTS` | string | `25` | Максимально допустимый CPU-request в конфиге execution задачи (валидация через `k8s.io/apimachinery/resource`) |
| `MAX_MEMORY_REQUESTS` | string | `300Gi` | Максимально допустимый memory-request в конфиге execution задачи |
## Переменные инфраструктуры, сборки и вспомогательных утилит
Переменные `Makefile` (значения по умолчанию, переопределяются через `make VAR=...`):
| Переменная | Значение по умолчанию | Назначение |
| --- | --- | --- |
| `OCI` | `docker` | Контейнерный движок (`docker`/`podman`) |
| `WF_API_CONTAINER__NETWORK_NAME` | `wf-api-network` | Имя bridge-сети для локальных контейнеров |
| `WF_API_DATABASE_IMAGE__TAG` | `wf-api-database` | Тег образа локальной БД |
| `WF_API_DATABASE_CONTAINER__NAME` | `wf-api-database-c` | Имя контейнера БД |
| `WF_API_DATABASE__VOLUME_NAME` | `wf-api-data` | Имя тома данных БД |
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` / `POSTGRES_PORT` | берутся из окружения | Прокидываются в контейнер БД при `container-run-database` |
Переменные сборки образа (`Dockerfile`):
| Переменная | Значение | Назначение |
| --- | --- | --- |
| `CGO_ENABLED` | `0` | Статическая сборка Go |
| `GOOS` | `linux` | Целевая ОС |
| `GOARCH` | `amd64` | Целевая архитектура |
Переменные `.docker/postgres/Dockerfile` и `docker-compose.yaml` (локальный стенд):
| Переменная | Значение | Назначение |
| --- | --- | --- |
| `POSTGRES_DB` | `processing_db` | БД локального PostgreSQL |
| `POSTGRES_USER` | `processing` | Пользователь локального PostgreSQL |
| `POSTGRES_PASSWORD` | `processing` | Пароль локального PostgreSQL |
| `POSTGRES_ADDRESS` | `database` (в compose для сервиса `api`) | Хост БД внутри сети compose |
| `POSTGRES_SSL_USE` | `false` | Отключение TLS локально |
## Переменные из Helm-чарта
Деплой выполняется зависимостью-чартом `universal-chart` (`.helm/Chart.yaml`, версия `0.1.7`), значения — в `.helm/values.yaml`. Значения даются по стендам через ключи `_default / stage / preprod / production`.
### Обычные переменные (`services.workflows-api.envs`)
| Переменная | Значение (`_default`) | Значения по стендам / примечание |
| --- | --- | --- |
| `POD_NAME` | `$(K8S_POD_NAME)` | Имя пода. **Кодом не читается** |
| `POSTGRES_POOL_SIZE` | `3` | Размер пула (логирование) |
| `HTTP_HOST` | `0.0.0.0:8080` | Адрес прослушивания в k8s (порт 8080) |
| `S3_SERVICE_ACCOUNT` | `/etc/sarex/yc-s3/yc-s3-service-account.json` | **Кодом не читается** |
| `DJANGO_HOST` | `https://stage.sarex.io` | stage: `stage.sarex.io`, preprod: `preprod.sarex.io`, production: `lk.sarex.io`. **Кодом не читается** |
| `OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT` | `opentelemetry-collector.observability.svc.cluster.local:4318` | Одинаково на всех стендах. **Кодом не читается** (легаси-пакет `observavility` не подключён) |
| `ENABLE_SQL_QUERY` | `0` | Читается в конфиг, но не используется |
| `POSTGRES_SSL_USE` | `1` | preprod: `true`, остальные `1` |
| `ENABLE_OBSERVABILITY` | `1` | stage `1`, preprod `0`, production `1`. **Кодом не читается** |
| `TRACER_USE` | `1` | Включает трейсинг |
| `TRACER_HOST` | `signoz-otel-collector.signoz.svc.cluster.local:4317` | preprod/production: `signoz-otel-collector-external.signoz.svc.cluster.local:4317` |
| `SERVICE_NAME` | `workflows-api.processing-stage` | stage: `workflows-api.platform`, preprod: `workflows-api.processing-preprod`, production: `workflows-api.processing-prod` |
| `TRACER_USE_INSECURE` | `1` | Небезопасное подключение к коллектору |
| `TRACER_LOGGER_NAME` | `tracer_logger` | Имя otel-логгера |
| `MAX_CPU_REQUESTS` | `25` | Лимит CPU-request задач |
| `MAX_MEMORY_REQUESTS` | `300Gi` | Лимит memory-request задач |
### Секретные переменные (`services.workflows-api.secretEnvs`)
| Переменная | Секрет (secret_name) | Ключ (secret_key) |
| --- | --- | --- |
| `POSTGRES_ADDRESS` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `host` |
| `POSTGRES_PORT` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `port` |
| `POSTGRES_DB` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `database` |
| `POSTGRES_USER` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `_default`/`stage`: `user`; `preprod`/`production`: `username` |
| `POSTGRES_PASSWORD` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `ya-pg-secret` | `password` |
| `PUBLIC_KEY` | `_default`/`stage`: `jwt-secret`; `preprod`/`production`: `public-key` | `_default`/`stage`: `public_key`; `preprod`/`production`: `key` |
| `YC-PG-CERTIFICATE` | `_default`/`stage`: `processing-postgresql-secret`; `preprod`/`production`: `yc-pg-certificate` | `_default`/`stage`: `ca.crt`; `preprod`/`production`: `certificate` |
### Прочие параметры чарта
- **Порт деплоймента**: `8080`; сервис `ClusterIP`, `targetPort: 8080`, `port: 80` (stage: `8000`).
- **Имя сервиса**: `workflows-service` (stage: `workflows-api-service`).
- **Реплики**: `_default`/`stage` — 1, preprod/production — 2.
- **Ресурсы пода**: requests `_default` 100Mi / 100m, preprod/production 200Mi / 200m.
- **Пробы**: liveness и readiness — `httpGet /ping` на порту 8080.
- **serviceAccount**: `workflows-api-sa`.
- **imagePullSecrets**: `dockerhub`. **Образ**: `cr.yandex/crp3ccidau046kdj8g9q/workflows-api`.
## Переменные в CI
`.gitlab-ci.yml` подключает общие пайплайны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`). Глобальные переменные:
| Переменная | Значение | Назначение |
| --- | --- | --- |
| `SERVICE_NAME` | `workflows-api` | Имя сервиса в пайплайне |
| `DOCKERFILE_PATH` | `Dockerfile` | Путь к Dockerfile |
| `BUILD_ARGS` | `--build-arg CI_COMMIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}` | Аргументы сборки образа |
| `CI_TRIGGER_SOURCE` | `app` | Источник триггера |
Маппинг ветка/тег → стенд и namespace (`workflow.rules`):
| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH | env (universal-chart.global.env) |
| --- | --- | --- | --- | --- | --- |
| `CI_COMMIT_BRANCH == "stage"` | `stage` | `platform` | `0.0.1-stage` | `universal-chart-stage` | `stage` |
| `CI_COMMIT_BRANCH == "master"` | `preprod` | `processing-preprod` | `0.0.1-preprod` | `universal-chart-preprod` | `preprod` |
| `CI_COMMIT_TAG` (любой тег) | `production` | `processing-prod` | `0.0.1-prod` | `universal-chart-production` | `production` |
| `CI_PIPELINE_SOURCE == "merge_request_event"` | — | — | — | — | сборка образа выключена (`ENABLE_BUILD_IMAGE=false`) |
Во всех деплой-правилах через `HELM_SET_ARGS` пробрасываются `IMAGE_NAME`, `commitSha=${CI_COMMIT_SHA}`, `gitlabUri`, `gitlabJobUrl`, `owner`. `RELEASE_NAME`/`CHART_NAME` — `workflows-api`.
Джоба `unittest` (`stage: test`, образ `golang:1.24`, `make unit-tests`) запускается на любых ветках/тегах и MR, `allow_failure: true`.
## Замечания и потенциальные проблемы
1. **Две разные структуры конфигурации БД с разными дефолтами.** Сервер использует `pkg/postgres/pgxconnection.Postgres` (дефолты `POSTGRES_USER=sarex`, `POSTGRES_PASSWORD=sarex`), а миграции — `pkg/postgres/gopg.Postgres` (дефолты `processing`/`processing`). При запуске без явно заданных переменных сервер и миграции подключались бы под разными кредами. В production это не проявляется, т. к. все переменные приходят из секретов.
2. **`ENABLE_SQL_QUERY` не используется.** Поле читается в обе структуры (`EnableSQLQuery`), но нигде в коде не применяется — флаг «мёртвый».
3. **`OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT`, `ENABLE_OBSERVABILITY` не используются.** Пакет `pkg/observavility` (с `SetupOTelSDK`) нигде не импортируется — это легаси. Реальный трейсинг настраивается переменными `TRACER_*` через внешнюю библиотеку `golang-fiber-otel-tools`.
4. **`POD_NAME`, `S3_SERVICE_ACCOUNT`, `DJANGO_HOST` из Helm кодом не читаются** — либо задел на будущее, либо устаревшие переменные.
5. **`entrypoint.sh` устарел и не используется.** Он ссылается на `/go/bin/migrations` и `/go/bin/httpserver`, тогда как в образе бинари лежат в `/httpserver` и `/migrations`, а `ENTRYPOINT` задан в `Dockerfile` напрямую (`/httpserver migrate`).
6. **Опечатка в mount-пути internal-middleware.** В `server.go` middleware, выставляющий `is_internal=true`, монтируется как `app.Use("./internal", ...)` (с ведущей точкой) вместо `"/internal"`. Из-за этого для маршрутов группы `/internal` флаг `is_internal` может не выставляться; в контроллерах `nil`-значение трактуется как «внутренний/доверенный запрос» (проверки принадлежности к компании пропускаются). Логически поведение сохраняется, но путь выглядит как баг.
7. **`PUBLIC_KEY` обязателен.** При пустом значении `auth.New` делает `panic("failed to parse PEM block ...")` — сервис не стартует. Дефолта нет.
8. **Расхождение по порту.** Дефолт кода `HTTP_HOST=0.0.0.0:8000`, docker-compose — `8000`, а в k8s (Helm) — `8080`. Локально сервис слушает 8000, в кластере — 8080.
9. **`BUILD_ARGS` передаёт `CI_COMMIT_SHORT_SHA`, но `Dockerfile` не объявляет соответствующий `ARG`** — build-arg игнорируется.
10. **`POSTGRES_SSL_USE` в Helm задаётся то как `1`, то как `true`** (preprod). cleanenv корректно парсит обе формы, но единообразия нет.
## Минимальный набор для локального запуска
Для запуска сервера локально (например, БД поднята через `make container-run-deps` или `docker-compose`) достаточно:
```env
# БД
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_DB=processing_db
POSTGRES_USER=processing
POSTGRES_PASSWORD=processing
POSTGRES_SSL_USE=false
# HTTP
HTTP_HOST=0.0.0.0:8000
# Обязательно: PEM публичный ключ (PKIX) для проверки JWT
PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
# Трейсинг можно выключить
TRACER_USE=false
```
Минимальный сценарий:
1. Поднять PostgreSQL: `make container-run-deps` (или `docker-compose up database`).
2. Экспортировать переменные выше (особенно валидный `PUBLIC_KEY`, иначе `panic`).
3. Прогнать миграции и запустить сервер: `go run ./cmd/httpserver migrate` (аргумент `migrate` включает миграции), либо `air` для hot-reload.
4. Проверить: `GET http://localhost:8000/ping``{"status":"ready"}`.
> Через `docker-compose up` сервис поднимается на `:8000`, БД — `processing/processing/processing_db`, но `PUBLIC_KEY` в compose не задан — для полноценной работы API его нужно добавить.

View File

@ -0,0 +1,68 @@
# ============================================================================
# workflows-api — пример переменных окружения
# Конфигурация читается через cleanenv (github.com/ilyakaznacheev/cleanenv)
# из переменных окружения. Префикса нет. Значения по умолчанию — из кода.
# ============================================================================
# ----- App -----
APP_NAME=workflows-api
APP_VERSION=v1
# ----- Logging -----
# Уровень логирования: debug | info | warn | error
LOG_LEVEL=info
# ----- HTTP -----
# Адрес и порт прослушивания (в k8s задаётся 0.0.0.0:8080)
HTTP_HOST=0.0.0.0:8000
# PEM публичный ключ (PKIX) для проверки JWT Sarex. ОБЯЗАТЕЛЕН:
# при пустом значении сервис падает с panic на старте.
PUBLIC_KEY=
# Максимальный размер тела запроса, байт (по умолчанию 256 MiB)
HTTP_BODY_LIMIT=268435456
# Размер буфера чтения, байт (по умолчанию 96 KiB)
HTTP_READ_BUFFER_SIZE=98304
# ----- Database (PostgreSQL) -----
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_DB=processing_db
POSTGRES_USER=processing
POSTGRES_PASSWORD=processing
# Размер пула (используется только для лога)
POSTGRES_POOL_SIZE=3
# Включить TLS-подключение к БД
POSTGRES_SSL_USE=false
# CA-сертификат (PEM) для TLS. Непустое значение включает TLS автоматически.
YC-PG-CERTIFICATE=
# ВНИМАНИЕ: переменная читается в конфиг, но кодом НЕ используется (мёртвый флаг).
ENABLE_SQL_QUERY=true
# ----- Tracing (OpenTelemetry) -----
# Включает трейсинг, otel-логгер и otelfiber-middleware
TRACER_USE=false
# Адрес OTLP-коллектора (gRPC)
TRACER_HOST=localhost:4317
# Небезопасное (без TLS) подключение к коллектору
TRACER_USE_INSECURE=true
# Имя сервиса в трейсах
SERVICE_NAME=workflows-api
# Имя otel-логгера
TRACER_LOGGER_NAME=tracer_logger
# ----- Execution (лимиты ресурсов задач) -----
# Максимально допустимый CPU-request в execution-конфиге задачи
MAX_CPU_REQUESTS=25
# Максимально допустимый memory-request в execution-конфиге задачи
MAX_MEMORY_REQUESTS=300Gi
# ============================================================================
# Переменные ниже присутствуют в .helm/values.yaml / старом .example.env,
# но кодом НЕ читаются (легаси). Оставлены для справки, включать не нужно:
# OBSERVABILITY_TRACE_COLLECTOR_ENDPOINT — пакет observavility не подключён
# ENABLE_OBSERVABILITY
# POD_NAME
# S3_SERVICE_ACCOUNT
# DJANGO_HOST
# ENABLE_SSL — опечатка старого .example.env; корректное имя POSTGRES_SSL_USE
# ============================================================================

View File

@ -0,0 +1,755 @@
openapi: 3.0.3
info:
title: Workflows API
version: "1.0.0"
description: |
REST API сервиса **workflows-api** для работы с workflow (пайплайнами задач):
создание, получение, перезапуск задач, отмена запусков, приоритизация и получение
позиции в очереди.
### Технологии
- Язык: Go 1.24, HTTP-фреймворк **Fiber v2**.
- JSON-сериализация: `bytedance/sonic`.
- Хранилище: PostgreSQL (пул `pgx/v5`).
- Трейсинг: OpenTelemetry (`otelfiber`), включается переменной `TRACER_USE`.
### Группы маршрутов
Все обработчики регистрируются дважды — в двух группах с одинаковым набором путей
(см. `internal/server/server.go` и `internal/controller/http/v1/workflows/routes.go`):
- **`/api/v1/...`** — публичная группа. На префикс `/api` навешен middleware аутентификации
(`auth.AuthMiddleware`): требуется валидный JWT. Для запросов выставляется `is_internal=false`
и проверяется принадлежность пользователя к компании.
- **`/internal/v1/...`** — внутренняя группа (сервис-сервис) без аутентификации; трактуется как
доверенная (`is_internal=true`), проверки принадлежности к компании пропускаются.
Отдельно, вне групп и без авторизации, доступен health-check **`GET /ping`**.
В этом документе пути описаны относительно базового префикса группы `/api/v1`
(см. `servers`). Те же пути доступны и под `/internal/v1`.
### Аутентификация
Группа `/api` защищена middleware, который принимает один из двух токенов:
- **`Authorization: Bearer <JWT>`** — токен Sarex, подпись проверяется публичным ключом
из переменной `PUBLIC_KEY` (RS/PKIX). Из claims извлекаются `user_id`, `company_ids`,
`is_superuser`.
- **`Identity: Bearer <JWT>`** — токен Zitadel (проверяется без верификации подписи,
`ParseUnverified`); данные пользователя берутся из claim
`urn:zitadel:iam:user:metadata` (поля `id`, `company_ids`, `is_superuser`).
Если присутствует заголовок `Identity`, используется он; иначе — `Authorization`.
Часть операций (`prioritize`, `move_to_super_high_resources`) доступна только суперпользователю
(`is_superuser=true`).
### Пагинация
Метод `GET /workflows` поддерживает `limit` и `offset` (query-параметры, строки).
Ответ содержит `items`, а также `limit`, `offset`, `total`.
### Обработка ошибок
Ошибки возвращаются в JSON вида:
```json
{ "message": "human readable message", "error_code": "WF-0001" }
```
Коды (`internal/app_errors`): `WF-0000` (system, 500), `WF-0001` (not found, 404),
`WF-0002` (no auth, 401), `WF-0003` (no access), `WF-0004` (invalid, 400),
`WF-0005` (workflow config error / forbidden, 400/403).
HTTP-статус выбирается в `ErrorHandler` фреймворка по типу ошибки: 400 (ошибки парсинга/валидации/
конфигурации workflow), 401 (нет/некорректный токен), 403 (forbidden), 404 (не найдено), 500 (прочее).
### Замечания (расхождения кода и существующей схемы)
Документ приведён в соответствие с реальными маршрутами `routes.go`. Отличия от старого
`openapi_schema.yaml`:
- **Добавлены** отсутствовавшие маршруты: `POST /workflows/batch`, `GET /workflows/{workflow_id}/position`,
`POST /workflows/{workflow_id}/prioritize`, `POST /tasks/{task_id}/move_to_super_high_resources`.
- **Удалён** маршрут `GET /tasks-runs/{task_run_id}/logs` — в коде такого обработчика нет.
(Внимание: `workflows-frontend` этот эндпоинт вызывает — см. `workflows-frontend.ENDPOINTS.md`.)
- `POST /tasks-runs/{task_run_id}/cancel` фактически возвращает пустой объект `{}` (в коде
`c.JSON(&struct{}{})`), а не объект task_run.
- У задачи (`Task`) в модели есть поля `execution`, `services`, `layer`, `id`, `workflow_id`,
а у workflow — `document_id`, которые в старой схеме отсутствовали.
servers:
- url: 'http://localhost:8000/api/v1'
description: Локальный сервер (дефолт HTTP_HOST / docker-compose)
- url: 'http://workflows-service/api/v1'
description: Внутрикластерный сервис (preprod/production; stage — workflows-api-service:8000)
- url: 'https://stage-api.sarex.io/workflows/api/v1'
description: Публичный stage (через ingress, префикс /workflows)
- url: 'https://api.sarex.io/workflows/api/v1'
description: Публичный production (через ingress, префикс /workflows)
tags:
- name: workflows
description: Операции с workflow
- name: tasks
description: Операции с задачами workflow
- name: task-runs
description: Операции с запусками задач
- name: health
description: Проверка доступности сервиса
paths:
/ping:
get:
tags: [health]
summary: Health-check
description: >
Проверка готовности сервиса. Зарегистрирован вне групп `/api` и `/internal`,
без аутентификации (реальный путь — `/ping`, без префикса `/api/v1`).
operationId: Ping
responses:
'200':
description: Сервис готов
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ready
/companies/{company_id}/workflows:
post:
tags: [workflows]
summary: Создать workflow
description: >
Создаёт workflow для компании. Для группы `/api` пользователь должен принадлежать
компании `company_id`. Задачи не должны содержать поле `services` — используется
`service_requests`. Если у задачи не задан `backoff_limit`, он проставляется равным 5.
Поле `valid_until` должно быть в будущем.
operationId: CreateWorkflow
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: company_id
in: path
required: true
description: Идентификатор компании
schema:
type: integer
example: 1
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWorkflowRequest'
responses:
'200':
description: Созданный workflow
content:
application/json:
schema:
$ref: '#/components/schemas/Workflow'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/workflows:
get:
tags: [workflows]
summary: Список workflow по компаниям
description: >
Возвращает список workflow для указанных компаний. Параметр `company_ids` —
обязательная строка с идентификаторами через запятую. Для группы `/api`
не-суперпользователю возвращаются только его компании.
operationId: ListByCompanyID
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: company_ids
in: query
required: true
description: Идентификаторы компаний через запятую
schema:
type: string
example: "1,2,3"
- name: limit
in: query
required: false
schema:
type: integer
example: 100
- name: offset
in: query
required: false
schema:
type: integer
example: 0
responses:
'200':
description: Список workflow
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowsResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/workflows/batch:
post:
tags: [workflows]
summary: Получить workflow пачкой
description: Возвращает workflow по списку идентификаторов.
operationId: GetWorkflows
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GetBatchWorkflowsRequest'
responses:
'200':
description: Найденные workflow
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowsBatchResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/workflows/{id}:
get:
tags: [workflows]
summary: Получить workflow по ID
operationId: GetWorkflow
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: id
in: path
required: true
description: UUID workflow
schema:
type: string
format: uuid
responses:
'200':
description: Workflow
content:
application/json:
schema:
$ref: '#/components/schemas/Workflow'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/workflows/{workflows_ids}/state:
get:
tags: [workflows]
summary: Состояния нескольких workflow
description: >
Возвращает отображение `workflow_id -> state` для списка workflow.
`workflows_ids` — UUID через запятую.
operationId: GetStates
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: workflows_ids
in: path
required: true
description: UUID workflow через запятую
schema:
type: string
example: "3fa85f64-5717-4562-b3fc-2c963f66afa6,4fa85f64-5717-4562-b3fc-2c963f66afa6"
responses:
'200':
description: Отображение id -> состояние
content:
application/json:
schema:
type: object
additionalProperties:
$ref: '#/components/schemas/CompletenessState'
example: { "3fa85f64-5717-4562-b3fc-2c963f66afa6": "done" }
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/workflows/{workflow_id}/position:
get:
tags: [workflows]
summary: Позиция workflow в очереди
description: Возвращает позицию workflow в очереди на выполнение (ограничение 100).
operationId: GetWorkflowQueuePosition
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: workflow_id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Позиция в очереди
content:
application/json:
schema:
type: object
properties:
position:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/workflows/{workflow_id}/prioritize:
post:
tags: [workflows]
summary: Приоритизировать workflow
description: Повышает приоритет workflow. Доступно только суперпользователю.
operationId: PrioritizeWorkflow
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: workflow_id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: Успешно, тело отсутствует
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/tasks/{task_id}/restart:
post:
tags: [tasks]
summary: Перезапустить задачу
description: >
Перезапускает задачу, опционально переопределяя `inputs`, `outputs`, `parameters`,
`valid_until`. Возвращает обновлённый workflow.
operationId: RestartTask
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: task_id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/RestartTaskRequest'
responses:
'200':
description: Обновлённый workflow
content:
application/json:
schema:
$ref: '#/components/schemas/Workflow'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/tasks/{task_id}/move_to_super_high_resources:
post:
tags: [tasks]
summary: Перевести задачу на super-high-resources
description: Перемещает задачу на пул ресурсов super-high-resources. Только суперпользователь.
operationId: MoveTaskToSuperHighResources
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: task_id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: Успешно, тело отсутствует
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/tasks-runs/{task_run_id}/cancel:
post:
tags: [task-runs]
summary: Отменить запуск задачи
description: Отменяет запуск задачи (task run). Возвращает пустой объект.
operationId: CancelTaskRun
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- name: task_run_id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Успешно (пустой объект)
content:
application/json:
schema:
type: object
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
components:
parameters:
AuthorizationHeader:
name: Authorization
in: header
required: true
description: >
`Bearer <JWT>` — токен Sarex. Альтернативно можно передать заголовок
`Identity: Bearer <JWT>` (токен Zitadel). Не требуется для группы `/internal`.
schema:
type: string
example: "Bearer eyJhbGciOi..."
responses:
BadRequest:
description: Некорректный запрос (парсинг/валидация/конфигурация workflow)
content:
application/json:
schema:
$ref: '#/components/schemas/AppError'
Unauthorized:
description: Не авторизован (нет/некорректный токен)
content:
application/json:
schema:
$ref: '#/components/schemas/AppError'
Forbidden:
description: Доступ запрещён
content:
application/json:
schema:
$ref: '#/components/schemas/AppError'
NotFound:
description: Ресурс не найден
content:
application/json:
schema:
$ref: '#/components/schemas/AppError'
InternalError:
description: Внутренняя ошибка
content:
application/json:
schema:
$ref: '#/components/schemas/AppError'
schemas:
AppError:
type: object
properties:
message:
type: string
example: "not found workflow"
error_code:
type: string
description: "Код ошибки: WF-0000..WF-0005"
example: "WF-0001"
CompletenessState:
type: string
description: Состояние workflow
enum: [done, running, error]
ExecutionState:
type: string
description: Состояние запуска задачи (task run)
enum: [pending, done, canceling, canceled, idle, running, error, lost]
Description:
type: object
description: Описание входа/выхода задачи (источник/приёмник данных)
required: [type, path]
properties:
type:
type: string
maxLength: 32
description: "Тип хранилища (валидация: google, local, s3, s3v2, srx-tmp, url, pdm)"
example: s3
path:
type: string
maxLength: 512
ServicePublicDescription:
type: object
properties:
type:
type: string
kind:
type: string
Resources:
type: object
properties:
cpu_limits:
type: string
memory_limits:
type: string
cpu_requests:
type: string
memory_requests:
type: string
Execution:
type: object
properties:
executor:
type: string
description: "Исполнитель задачи (допустимые: k8s, amqp)"
enum: [k8s, amqp]
resources:
$ref: '#/components/schemas/Resources'
Task:
type: object
properties:
id:
type: string
format: uuid
workflow_id:
type: string
format: uuid
slug:
type: string
docker_image:
type: string
backoff_limit:
type: integer
format: int32
description: "По умолчанию 5, если не задан"
inputs:
type: object
additionalProperties:
$ref: '#/components/schemas/Description'
outputs:
type: object
additionalProperties:
$ref: '#/components/schemas/Description'
parameters:
type: object
additionalProperties: true
service_requests:
type: array
items:
type: string
services:
type: array
items:
$ref: '#/components/schemas/ServicePublicDescription'
execution:
$ref: '#/components/schemas/Execution'
needs:
type: array
items:
type: string
layer:
type: integer
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
TaskRun:
type: object
properties:
id:
type: string
format: uuid
task_id:
type: string
format: uuid
workflow_id:
type: string
format: uuid
state:
$ref: '#/components/schemas/ExecutionState'
reason:
type: string
nullable: true
progress:
type: integer
format: int32
nullable: true
total_time:
type: integer
format: int32
nullable: true
logs_paths:
type: array
items:
type: string
logs_storage:
type: string
nullable: true
logs_last_gathered:
type: string
format: date-time
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
Workflow:
type: object
properties:
id:
type: string
format: uuid
company_id:
type: integer
state:
$ref: '#/components/schemas/CompletenessState'
name:
type: string
valid_until:
type: string
format: date-time
document_id:
type: integer
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
task_runs:
type: array
items:
$ref: '#/components/schemas/TaskRun'
CreateWorkflowRequest:
type: object
required: [name, valid_until]
properties:
name:
type: string
valid_until:
type: string
format: date-time
document_id:
type: integer
nullable: true
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
RestartTaskRequest:
type: object
properties:
inputs:
type: object
additionalProperties:
$ref: '#/components/schemas/Description'
outputs:
type: object
additionalProperties:
$ref: '#/components/schemas/Description'
parameters:
type: object
additionalProperties: true
valid_until:
type: string
format: date-time
GetBatchWorkflowsRequest:
type: object
required: [workflows_ids]
properties:
workflows_ids:
type: array
items:
type: string
format: uuid
WorkflowsBatchResponse:
type: object
properties:
workflows:
type: array
items:
$ref: '#/components/schemas/Workflow'
WorkflowsResponse:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Workflow'
limit:
type: integer
offset:
type: integer
total:
type: integer

Some files were not shown because too many files have changed in this diff Show More