Add example .env files and detailed configuration documentation for mapper, message-hub, notes, pm, and prescriptions services.

This commit is contained in:
emelinda 2026-07-13 23:48:41 +03:00
parent b18e31d5d6
commit 9cff5d6e39
26 changed files with 6620 additions and 0 deletions

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,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

View File

@ -0,0 +1,363 @@
# Конфигурация проекта workflows-engine
`workflows-engine` (внутреннее имя образа — `kubernetes-engine`) — это фоновый сервис-оркестратор (демон без REST API), который вычитывает workflow/задачи из PostgreSQL и запускает их либо как Job'ы в Kubernetes, либо публикует в RabbitMQ (AMQP-исполнитель). Конфигурируется исключительно через переменные окружения.
## Способы конфигурирования
- **Библиотека парсинга:** [`github.com/ilyakaznacheev/cleanenv`](https://github.com/ilyakaznacheev/cleanenv).
- **Точка входа конфигурации:** `config/config.go`, функция `config.New()` вызывает `cleanenv.ReadEnv(cfg)` и заполняет структуру `Config` из переменных окружения процесса.
- **Формат тегов:** каждое поле помечено тегом `env:"ИМЯ"`; значение по умолчанию задаётся тегом `env-default:"..."`. Префикса имён переменных нет.
- **Составная структура `Config`** собирается из встроенных (embedded) структур:
- `App`, `Log`, `Executor`, `Kubernetes`, `Resources`, `Storages`, `Pooling`, `Workflows`, `Security` — объявлены в `config/config.go`;
- `pgxconnection.Postgres` — объявлена в `pkg/pgxconnection/postgres.go` (переменные `POSTGRES_*`);
- `rabbitmq.RabbitMQ` — объявлена в `pkg/rabbitmq/config.go` (переменные `RABBITMQ_*`).
- **Кастомный парсинг:** поле `WORKFLOW_PRIORITY` имеет тип `entity.WorkflowPriority` с методом `SetValue` (`internal/entity/workflow.go`); допустимые значения: `default`, `high`, `low` (регистр не важен).
- **Важно:** часть переменных из `config.env` и Helm-чарта **не читается** структурой конфигурации. Они либо читаются напрямую через `os.Getenv(...)` в `pkg/kube_services/services.go` и **пробрасываются в env запускаемых Job-подов** (а не потребляются самим engine), либо не используются кодом вовсе (см. раздел «Замечания и потенциальные проблемы»).
### Режимы запуска / исполнители
Бинарь один (`cmd/engine`), «режим» определяется набором включённых исполнителей и приоритетом.
| Режим | Как включается | Назначение |
| --- | --- | --- |
| Kubernetes-исполнитель | `ENABLE_KUBERNETES_EXECUTOR=1` (по умолчанию `true`) | Запуск задач как `batchv1.Job` в кластере Kubernetes |
| AMQP-исполнитель | `ENABLE_AMQP_EXECUTOR=1` (по умолчанию `false`) | Публикация задач в RabbitMQ и приём результатов |
| backend (обычный приоритет) | Helm-деплой `backend`, `WORKFLOW_PRIORITY=low` | Обработка обычных workflow |
| backend-high-priority | Helm-деплой `backend-high-priority`, `WORKFLOW_PRIORITY=high` | Обработка высокоприоритетных workflow |
Оба Helm-деплоя разворачивают один и тот же образ; отличаются только значением `WORKFLOW_PRIORITY``MAX_WORKFLOWS_LIMIT`).
### Точки входа (entrypoints)
| Путь | Тип | Описание |
| --- | --- | --- |
| `cmd/engine/main.go` | main-пакет | Единственная точка входа. Поднимает pprof-сервер на `:8081`, читает конфиг, создаёт `engine.New(cfg, logger)` и вызывает `Run()` |
| `internal/apps/engine/engine.go` | приложение | Основная логика: подключение к Postgres, инициализация исполнителей (K8s/AMQP), pooling-контроллер и оркестратор, graceful shutdown по SIGINT |
### Запуск контейнера
- **Dockerfile:** multi-stage сборка на `golang:1.20-buster`, финальный образ `FROM scratch`.
- **CMD/ENTRYPOINT:** `ENTRYPOINT ["/engine"]` — запускается собранный бинарь напрямую, без shell-скрипта/entrypoint-обёртки.
- Внутри процесса дополнительно поднимается HTTP-сервер `net/http/pprof` на порту `:8081` (профилирование). Полноценного REST API нет (см. раздел про `API_ADDRESS`).
## Переменные приложения
### Приложение и логирование (`App`, `Log`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `APP_NAME` | string | `kubernetes-engine` | Имя приложения (в Helm переопределяется на `kubernetes-engine`/`kubernetes-engine-high-priority`) |
| `APP_VERSION` | string | `v1` | Версия приложения |
| `LOG_LEVEL` | string | `info` | Уровень логирования |
### PostgreSQL (`pkg/pgxconnection`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `POSTGRES_ADDRESS` | string | `localhost` | Хост БД |
| `POSTGRES_DB` | string | `processing_db` | Имя базы данных |
| `POSTGRES_USER` | string | `user` | Пользователь БД |
| `POSTGRES_PASSWORD` | string | `password` | Пароль БД |
| `POSTGRES_PORT` | string | `5432` | Порт БД |
| `POSTGRES_POOL_SIZE` | int | `20` | Размер пула соединений |
| `ENABLE_SQL_QUERY` | bool | `true` | Логирование SQL-запросов |
| `YC-PG-CERTIFICATE` | string | `""` | TLS-сертификат CA для подключения к БД (значение сертификата) |
| `POSTGRES_SSL_USE` | bool | `false` | Использовать SSL при подключении к БД |
### RabbitMQ (`pkg/rabbitmq`) — используется только при `ENABLE_AMQP_EXECUTOR=1`
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `RABBITMQ_HOST` | string | — | Хост брокера |
| `RABBITMQ_PORT` | string | — | Порт брокера |
| `RABBITMQ_USER` | string | — | Пользователь |
| `RABBITMQ_PASS` | string | — | Пароль |
| `RABBITMQ_VHOST` | string | — | Виртуальный хост |
| `RABBITMQ_USE_SSL` | bool | — | Использовать TLS |
| `RABBITMQ_CERTIFICATE` | string | — | TLS-сертификат |
| `RABBITMQ_CREATE_EXCHANGE` | string | — | Exchange для отправки задач на запуск |
| `RABBITMQ_CANCEL_EXCHANGE` | string | — | Exchange для отмены задач |
| `RABBITMQ_CREATE_ROUTING_KEY` | string | — | Routing key для запуска |
| `RABBITMQ_CANCEL_TOPIC` | string | — | Topic/ключ отмены |
| `RABBITMQ_COMPLETENESS_EXCHANGE` | string | — | Exchange для получения результатов выполнения |
| `RABBITMQ_COMPLETENESS_TOPIC` | string | — | Topic результатов выполнения |
> Примечание: в `config.env` присутствуют также `RABBITMQ_CREATE_TOPIC` — переменной `RABBITMQ_CREATE_TOPIC` в коде нет (в структуре есть только `CreateRoutingKey`).
### Исполнители и воркеры (`Executor`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `COUNT_RUNNING_WORKERS` | int | — (0) | Кол-во воркеров запуска задач |
| `COUNT_CANCELING_WORKERS` | int | — (0) | Кол-во воркеров отмены задач |
| `COUNT_HANDLE_JOB_WORKERS` | int | — (0) | Кол-во воркеров обработки Job'ов |
| `ENABLE_AMQP_EXECUTOR` | bool | `false` | Включить AMQP-исполнитель (RabbitMQ) |
| `ENABLE_KUBERNETES_EXECUTOR` | bool | `true` | Включить Kubernetes-исполнитель |
### Kubernetes (`Kubernetes`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `JOBS_NAMESPACE` | string | — | Namespace, в котором создаются Job'ы задач |
| `KUBE_CONFIG` | string | — | Путь к kubeconfig. Если **пусто** — используется in-cluster конфигурация; если задан — out-of-cluster |
| `KUBE_CONTEXT` | string | — | Контекст kubeconfig (для out-of-cluster) |
| `KUBE_ADDR` | string | — | Адрес API-сервера кластера (для out-of-cluster; используется с `InsecureSkipTLSVerify`) |
### Ресурсы и планирование подов (`Resources`)
Управляют NodeSelector/Tolerations/requests для запускаемых Job'ов в зависимости от `service_requests` задачи (см. `README.md`).
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `CPU_COUNT` | int | — | CPU requests для `high-resources`/`persistent` |
| `MEMORY_GI` | int | — | Memory (Gi) requests для `high-resources`/`persistent` |
| `CPU_COUNT_LOW_RESOURCES` | int | — | CPU для `low-resources` |
| `MEMORY_GI_LOW_RESOURCES` | int | — | Memory (Gi) для `low-resources` |
| `CPU_COUNT_HIGH_MEM` | int | — | CPU для `super-high-resources` |
| `MEMORY_GI_HIGH_MEM` | int | — | Memory (Gi) для `super-high-resources` |
| `ENABLE_TOLERATION` | bool | — | Включить набор сервисов ресурсов (toleration/nodeselector) |
| `TOLERATION_KEY` | string | — | Ключ toleration/nodeselector для high/low ресурсов |
| `TOLERATION_VALUE` | string | — | Значение toleration/nodeselector для high/low ресурсов |
| `TOLERATION_KEY_HIGH_MEM` | string | — | Ключ toleration для `super-high-resources` |
| `TOLERATION_VALUE_HIGH_MEM` | string | — | Значение toleration для `super-high-resources` |
| `TOLERATION_KEY_PERSISTENT` | string | — | Ключ toleration для `persistent` |
| `TOLERATION_VALUE_PERSISTENT` | string | — | Значение toleration для `persistent` |
| `DJANGO_BASIC_AUTH` | string | — | Базовая авторизация Django (поле присутствует в конфиге, но не используется в бизнес-логике; в под задачи прокидывается хардкод-путь `DJANGO_BASIC_AUTH_PATH`) |
| `DEFAULT_TOLERATION_KEY` | string | — | Ключ toleration по умолчанию для всех подов |
| `DEFAULT_TOLERATION_VALUE` | string | — | Значение toleration по умолчанию |
| `DEFAULT_NODE_SELECTOR_KEY` | string | — | Ключ NodeSelector по умолчанию |
| `DEFAULT_NODE_SELECTOR_VALUE` | string | — | Значение NodeSelector по умолчанию |
| `DEFAULT_IMAGE_PULL_POLICY` | string | `always` | ImagePullPolicy для подов задач |
| `DEFAULT_CPU_REQUESTS` | string | `100m` | CPU requests по умолчанию |
| `DEFAULT_MEMORY_REQUESTS` | string | `64Mi` | Memory requests по умолчанию |
### Хранилища и внешние сервисы (`Storages`)
Флаги `ENABLE_*` включают соответствующий «сервис» (набор env/секретов/volume), который добавляется в под задачи в зависимости от `service_requests`. URL'ы прокидываются в поды mesh/pdm-сервисов.
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `S3_SERVICE_ACCOUNT` | string | — | Путь к service account JSON для S3 (в коде для подов путь по факту хардкодится) |
| `ENABLE_S3_STORAGE` | bool | — | Включить сервис `s3` |
| `ENABLE_S3V2_STORAGE` | bool | — | Включить сервис `s3v2` |
| `ENABLE_PDM_STORAGE` | bool | — | Включить сервис `pdm` |
| `ENABLE_URL_STORAGE` | bool | — | Включить сервис `url` |
| `ENABLE_LOCAL` | bool | — | Включить локальное хранилище |
| `ENABLE_BIM_API_DB` | bool | — | Включить сервис `bim_api_db` |
| `ENABLE_BIM_API_CH` | bool | — | Включить сервис `bim_api_ch` (ClickHouse) |
| `ENABLE_BIM_API_V2_DB` | bool | — | Включить сервисы `bim_api_v2_db` (+ db_2/3/4) |
| `ENABLE_PDM_API_DB` | bool | — | Включить сервис `pdm_api_db` |
| `ENABLE_COMPARISONS_API_DB` | bool | — | Включить сервис `comparison-api-db` |
| `ENABLE_ISSUE_API_DB` | bool | — | Включить сервис `issues-api-db` |
| `ENABLE_RESOURCES_API` | bool | — | Включить сервис `resources-api` |
| `ENABLE_MAIL_GUN` | bool | — | Включить сервис `mailgun` |
| `ENABLE_SMTP` | bool | — | Включить сервис `smtp` |
| `ENABLE_WORKSPACE_API_DB` | bool | — | Включить сервис `workspace_api_db` |
| `ENABLE_CROSS_SECTION_API_DB` | bool | — | Включить сервис `cross-section-api-db` |
| `BIM_API_DEBUG` | bool | — | Флаг debug для BIM API (также пробрасывается в под, см. ниже) |
| `COMPARISONS_API_DEBUG` | string | — | Флаг debug для Comparisons API (также пробрасывается в под) |
| `INTERNAL_PDM_URL` | string | — | Внутренний URL PDM |
| `EXTERNAL_PDM_URL` | string | — | Внешний URL PDM |
| `INTERNAL_FILESTREAM_URL` | string | — | Внутренний URL filestream |
| `EXTERNAL_FILESTREAM_URL` | string | — | Внешний URL filestream |
| `INTERNAL_REMARK_URL` | string | — | Внутренний URL remarks |
| `EXTERNAL_REMARK_URL` | string | — | Внешний URL remarks |
| `DJANGO_HOST` | string | — | URL Django-хоста (ЛК) |
| `INTERNAL_WORKSPACE_URL` | string | — | Внутренний URL workspaces |
| `EXTERNAL_WORKSPACE_URL` | string | — | Внешний URL workspaces |
### Пулинг (`Pooling`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `MAX_WORKFLOWS_LIMIT` | uint64 | — (0) | Лимит одновременно обрабатываемых workflow при выборке |
| `MAX_RUNNING_JOBS` | int64 | `200` | Максимум одновременно выполняющихся Job'ов |
### Workflow (`Workflows`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `DEFAULT_GET_SINCE_LAST_HOURS` | uint64 | `168` | Глубина выборки задач в часах (нужно для FIFO; 168 ч = 7 дней) |
| `WORKFLOW_PRIORITY` | enum (`default`/`high`/`low`) | `default` | Приоритет обрабатываемых workflow (задаёт «роль» инстанса) |
### Безопасность (`Security`)
| Переменная | Тип | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `ENABLE_RUNASUSER` | bool | `false` | Запускать контейнеры задач под фиксированным UID |
| `USER_RUNASUSER` | int64 | `1000` | UID для `runAsUser` |
### Переменные, читаемые через `os.Getenv` и пробрасываемые в под'ы задач
Эти переменные **не входят** в структуру `Config` и **не потребляются** самим engine. Они читаются напрямую в `pkg/kube_services/services.go` и добавляются как env в контейнер запускаемой задачи (Job), когда включён соответствующий `ENABLE_*`-флаг. Тип — всегда строка (передаётся «как есть»).
| Переменная | Куда прокидывается (условие) | Назначение |
| --- | --- | --- |
| `BIM_API_DB` | сервис `bim_api_db` (при `ENABLE_BIM_API_DB`) | Путь к JSON-конфигу БД BIM API |
| `BIM_API_DEBUG` | сервис `bim_api_db` | Флаг debug |
| `BIM_API_CH` | сервис `bim_api_ch` (при `ENABLE_BIM_API_CH`) | Путь к JSON-конфигу ClickHouse |
| `BIM_API_CH_DEBUG` | сервис `bim_api_ch` | Флаг debug |
| `BIM_API_V2_DB` | сервис `bim_api_v2_db` (при `ENABLE_BIM_API_V2_DB`) — в под прокидывается под именем `BIM_API_DB` | Путь к JSON-конфигу БД BIM API v2 |
| `BIM_API_V2_DEBUG` | сервис `bim_api_v2_db` | Флаг debug |
| `BIM_API_V2_DB_2` | сервис `bim_api_v2_db_2` — в под как `BIM_API_DB_2` | Путь к JSON-конфигу 2-й БД BIM v2 |
| `BIM_API_V2_DB_3` | сервис `bim_api_v2_db_3` — в под как `BIM_API_DB_3` | Путь к JSON-конфигу 3-й БД BIM v2 |
| `BIM_API_V2_DB_4` | сервис `bim_api_v2_db_4` — в под как `BIM_API_DB_4` | Путь к JSON-конфигу 4-й БД BIM v2 |
| `COMPARISONS_API_DB` | сервис `comparison-api-db` (при `ENABLE_COMPARISONS_API_DB`) | Путь к JSON-конфигу БД Comparisons |
| `COMPARISONS_API_DEBUG` | сервис `comparison-api-db` | Флаг debug |
| `ISSUE_API_DB` | сервис `issues-api-db` (при `ENABLE_ISSUE_API_DB`) | Путь к JSON-конфигу БД Issues |
| `ISSUE_API_DEBUG` | сервис `issues-api-db` | Флаг debug |
| `RESOURCES_API_INTERNAL_HOST` | сервис `resources-api` (при `ENABLE_RESOURCES_API`) | Внутренний хост Resources API |
| `SMTP` | сервис `smtp` (при `ENABLE_SMTP`) — в под как `SMTP_CONFIG_PATH` | Путь к JSON-конфигу SMTP |
| `PDM_API_DB` | сервис `pdm_api_db` (при `ENABLE_PDM_API_DB`) | Путь к JSON-конфигу БД PDM |
| `PDM_API_DEBUG` | сервис `pdm_api_db` | Флаг debug |
| `WORKSPACE_API_DB` | сервис `workspace_api_db` (при `ENABLE_WORKSPACE_API_DB`) | Путь к JSON-конфигу БД Workspace |
| `WORKSPACE_API_DEBUG` | сервис `workspace_api_db` | Флаг debug |
| `CROSS_SECTION_API_DB` | сервис `cross-section-api-db` (при `ENABLE_CROSS_SECTION_API_DB`) | Путь к JSON-конфигу БД Cross-section |
| `CROSS_SECTION_API_DEBUG` | сервис `cross-section-api-db` | Флаг debug |
| `WORKFLOWS_SENTRY_DSN` | сервис `Default` (всегда) | Sentry DSN для подов задач |
| `WORKFLOWS_SENTRY_DEBUG` | сервис `Default` (всегда) | Флаг debug Sentry |
| `ENVIRONMENT` | сервис `Default` (всегда) | Имя окружения для подов задач |
> Пути `BIM_API_V2_DB_2/3/4`, `MAILGUN`, `SMTP` и т.п. задаются в Helm-чарте (`.helm/values.yaml`) — в `config.env` присутствуют не все из них.
## Переменные инфраструктуры/сборки
| Переменная | Где | Назначение |
| --- | --- | --- |
| `CGO_ENABLED=0`, `GOOS=linux`, `GOARCH=amd64` | Dockerfile (build stage) | Статическая сборка бинаря под Linux/amd64 |
| `CI_COMMIT_SHORT_SHA` | `.gitlab-ci.yml` → build-arg | Прокидывается в сборку образа |
| `SERVICE_NAME=workflows-engine` | `.gitlab-ci.yml` | Имя сервиса/чарта в CI |
| `DOCKERFILE_PATH=Dockerfile` | `.gitlab-ci.yml` | Путь к Dockerfile |
| `BUILD_ARGS` | `.gitlab-ci.yml` | Аргументы сборки образа |
| `CI_TRIGGER_SOURCE=app` | `.gitlab-ci.yml` | Источник триггера пайплайна |
| `IMAGE_NAME`, `CI_COMMIT_SHA`, `CI_PROJECT_URL`, `CI_JOB_URL`, `CI_PROJECT_NAMESPACE` | `.gitlab-ci.yml``HELM_SET_ARGS` | Метаданные деплоя, пробрасываются в universal-chart |
Сборка/деплой наследуются из внешних CI-шаблонов проекта `generic/common-ci` (`universal-pipeline.yaml`, `common-security-scan.yaml`). Юнит-тесты: стадия `test`, образ `golang:1.20`, команда `make unit-tests`.
## Переменные из Helm-чарта
Чарт (`.helm/values.yaml`) построен на `universal-chart`; секреты монтируются как env (`secretEnvs`) и как volume'ы. Ниже — маппинг секретных env (общий для `backend` и `backend-high-priority`). Имя секрета зависит от окружения (`_default` / `stage` / `preprod` / `production`).
| Переменная | Секрет (secret_name) | Ключ (secret_key) |
| --- | --- | --- |
| `POSTGRES_ADDRESS` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `host` |
| `POSTGRES_PORT` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `port` |
| `POSTGRES_DB` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `database` |
| `POSTGRES_USER` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `username` |
| `POSTGRES_PASSWORD` | `ya-pg-secret` (stage: `processing-postgresql-secret`) | `password` |
| `RABBITMQ_USER` | `rabbitmq-secret` | `username` |
| `RABBITMQ_PASS` | `rabbitmq-secret` | `password` |
| `YC-PG-CERTIFICATE` | `yc-pg-certificate` (stage: `processing-postgresql-secret`) | `certificate` (stage: `ca.crt`, preprod/prod: `certificate`) |
Секреты, монтируемые как volume'ы (файлы JSON-конфигов внешних БД и т.п.):
| Volume / секрет | mountPath |
| --- | --- |
| `yc-s3` | `/etc/sarex/yc-s3` |
| `bim-api-db` | `/etc/sarex` |
| `bim-api-v2-db-2` | `/etc/sarex/second-bim-bd` |
| `bim-api-v2-db-3` | `/etc/sarex/third-bim-bd` |
| `bim-api-v2-db-4` | `/etc/sarex/fourth-bim-bd` |
| `comparison-api-db` | `/etc/comparisons` |
| `pdm-api-db` | `/etc/pdm` |
| `ws-api-db` | `/etc/ws` |
| `mailgun-secret` | `/etc/mailgun-secret` |
| `smtp-secret` | `/etc/smtp-secret` |
| `issues-api-db` | `/etc/issues` |
| `cross-section-api-db` | `/etc/cross_section` |
| `tmp-volume` (emptyDir) | `/tmp` |
Прочие значимые переменные окружения задаются в `.helm/values.yaml` секцией `envs` для каждого сервиса (значения зависят от окружения `_default/stage/preprod/production`) — в т.ч. пути к JSON-конфигам (`BIM_API_DB`, `PDM_API_DB`, …), URL'ы сервисов, `POD_NAME=$(K8S_POD_NAME)`, `WORKFLOW_PRIORITY` (`low` для `backend`, `high` для `backend-high-priority`). Также в чарте описан `meshConfig` (Istio) — включён только для production.
## Переменные в CI
Определяются в `workflow.rules` файла `.gitlab-ci.yml` по ветке/тегу:
| Триггер | STAND | NAMESPACE | universal-chart env | CHART_VERSION | K8S_HUSTLER_BRANCH |
| --- | --- | --- | --- | --- | --- |
| ветка `stage` | `stage` | `platform` | `stage` | `0.0.1-stage` | `universal-chart-stage` |
| ветка `master` | `preprod` | `processing-preprod` | `preprod` | `0.0.1-preprod` | `universal-chart-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `processing-prod` | `production` | `0.0.1-prod` | `universal-chart-production` |
| Merge Request | — (сборка образа выключена: `ENABLE_BUILD_IMAGE=false`) | — | — | — | — |
`RELEASE_NAME` во всех случаях — `workflows-engine`. Для каждого стенда деплоятся оба сервиса: `backend` и `backend-high-priority`.
## Замечания и потенциальные проблемы
1. **Нет HTTP REST API, но `API_ADDRESS` присутствует.** Переменная `API_ADDRESS=0.0.0.0:8080` есть в `config.env` и в Helm (`envs`), но **не читается ни одной строкой Go-кода**. Реальный HTTP-сервер — только `net/http/pprof` на `:8081` (`cmd/engine/main.go`). В Helm у сервисов `service.enabled: false`, liveness/readiness-пробы выключены. Переменную стоит либо удалить, либо реализовать сервер. По этой причине `openapi.yaml` для сервиса не создаётся.
2. **Дубликаты в `config.env`:**
- `ENABLE_BIM_API_V2_DB=1` указана дважды (строки 18 и 19);
- `JOBS_NAMESPACE` задаётся дважды с разными значениями: `processing-stage` и `processing-testing` — побеждает последнее;
- `POD_NAME` задаётся дважды: `fieldRef(v1:metadata.name)` и `workflows-backend-869584d795-b7p2b` — второе значение это «замороженное» имя конкретного пода, что явно ошибочно для шаблона.
3. **Хардкод локальных путей разработчика в `config.env`:**
- `S3_SERVICE_ACCOUNT=/Users/khannanov/sarex/yc-s3-service-account.json`;
- `KUBE_CONFIG=/Users/khannanov/.kube/config`.
Эти пути специфичны для машины конкретного разработчика и не должны попадать в общий конфиг (в Helm `S3_SERVICE_ACCOUNT` корректно указывает на `/etc/sarex/yc-s3/...`).
4. **Переменные из `config.env`/Helm, которые не читаются кодом `engine` вообще** (кандидаты на удаление либо потребляются исключительно другими компонентами): `API_ADDRESS`, `CONTROL_PLANE_PERIOD`, `ENABLE_GOOGLE_STORAGE`, `GOOGLE_STORAGE_BUCKET`, `GOOGLE_STORAGE_PROJECT`, `ENABLE_SRX_TMP`, `MAX_TASKS_IN_PERIOD`, `ENABLE_PDM_DB`, `POD_NAME`. Ни `Getenv`, ни struct-тега для них нет.
5. **`config.env` частично устарел относительно `config/config.go` и Helm.** Ряд переменных, реально используемых кодом/чартом, в `config.env` отсутствует (`WORKFLOW_PRIORITY`, `DEFAULT_TOLERATION_*`, `DEFAULT_NODE_SELECTOR_*`, `ENABLE_ISSUE_API_DB`, `ENABLE_RESOURCES_API`, `ENABLE_WORKSPACE_API_DB`, `ENABLE_CROSS_SECTION_API_DB`, `RESOURCES_API_INTERNAL_HOST`, `WORKFLOWS_SENTRY_DSN`, пути `*_API_DB` для issues/ws/cross_section и т.д.). Источником истины следует считать `config/config.go` + `.helm/values.yaml`, а не `config.env`.
6. **Расхождение `env-default` и реальных значений.** У ряда флагов (`ENABLE_S3_STORAGE`, `ENABLE_TOLERATION`, `CPU_COUNT`, воркеры и др.) в коде нет `env-default`, поэтому при отсутствии переменной поле останется нулевым (`0`/`false`/`""`) — сервис молча стартует с «пустой» конфигурацией планирования. Значения обязательно должны задаваться через окружение (Helm).
7. **`DJANGO_BASIC_AUTH` / `COMPARISONS_API_DEBUG` / `BIM_API_DEBUG`** объявлены в структуре конфига, но фактически в бизнес-логике engine не задействованы (debug-флаги повторно читаются через `os.Getenv` и прокидываются в поды; `DJANGO_BASIC_AUTH` не используется — в под задачи попадает хардкод-путь `DJANGO_BASIC_AUTH_PATH=/etc/sarex/django-auth.json`).
8. **`RABBITMQ_CREATE_TOPIC`** присутствует в `config.env`, но в конфиге RabbitMQ такого поля нет (используется `RABBITMQ_CREATE_ROUTING_KEY`) — переменная не читается.
## Минимальный набор для локального запуска
Для локального старта достаточно поднять PostgreSQL и указать доступ к нему; исполнители и внешние интеграции можно выключить.
```env
# Логирование
LOG_LEVEL=debug
# PostgreSQL (обязательно — engine сразу подключается к БД)
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_DB=processing_db
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_POOL_SIZE=5
POSTGRES_SSL_USE=false
ENABLE_SQL_QUERY=true
# Исполнители: выключаем AMQP; для Kubernetes нужен доступ к кластеру
ENABLE_AMQP_EXECUTOR=0
ENABLE_KUBERNETES_EXECUTOR=1
KUBE_CONFIG=/path/to/your/.kube/config # если пусто — используется in-cluster
KUBE_CONTEXT=your-context
JOBS_NAMESPACE=processing-testing
# Воркеры
COUNT_RUNNING_WORKERS=1
COUNT_CANCELING_WORKERS=1
COUNT_HANDLE_JOB_WORKERS=1
# Пулинг / выборка
MAX_WORKFLOWS_LIMIT=10
MAX_RUNNING_JOBS=10
DEFAULT_GET_SINCE_LAST_HOURS=168
WORKFLOW_PRIORITY=default
# Дефолты планирования подов (иначе будут пустыми)
DEFAULT_IMAGE_PULL_POLICY=IfNotPresent
DEFAULT_CPU_REQUESTS=100m
DEFAULT_MEMORY_REQUESTS=64Mi
# Хранилища/внешние БД можно отключить для минимального запуска
ENABLE_S3_STORAGE=0
ENABLE_S3V2_STORAGE=0
ENABLE_PDM_STORAGE=0
ENABLE_URL_STORAGE=0
ENABLE_LOCAL=0
ENABLE_BIM_API_DB=0
ENABLE_BIM_API_CH=0
ENABLE_BIM_API_V2_DB=0
ENABLE_PDM_API_DB=0
ENABLE_COMPARISONS_API_DB=0
ENABLE_ISSUE_API_DB=0
ENABLE_RESOURCES_API=0
ENABLE_MAIL_GUN=0
ENABLE_SMTP=0
ENABLE_WORKSPACE_API_DB=0
ENABLE_CROSS_SECTION_API_DB=0
ENABLE_TOLERATION=0
```
Запуск: `go run ./cmd/engine`. Если Kubernetes-исполнитель не нужен вовсе — установите `ENABLE_KUBERNETES_EXECUTOR=0` (тогда `KUBE_*`/`JOBS_NAMESPACE` не требуются), но учтите, что без единого включённого исполнителя сервис не будет запускать задачи. Профилирование доступно на `http://localhost:8081/debug/pprof/`.

View File

@ -0,0 +1,185 @@
# ============================================================================
# workflows-engine (kubernetes-engine) — пример переменных окружения
# Конфигурация читается через cleanenv (github.com/ilyakaznacheev/cleanenv)
# из переменных окружения. Префикса нет. Значения по умолчанию — из кода.
# Сервис — фоновый оркестратор без REST API (только pprof на :8081).
# ============================================================================
# ----- App / Logging -----
APP_NAME=kubernetes-engine
APP_VERSION=v1
# Уровень логирования: debug | info | warn | error
LOG_LEVEL=info
# ----- Database (PostgreSQL) -----
POSTGRES_ADDRESS=localhost
POSTGRES_PORT=5432
POSTGRES_DB=processing_db
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_POOL_SIZE=20
# Включить TLS-подключение к БД
POSTGRES_SSL_USE=false
# CA-сертификат (PEM) для TLS-подключения к БД (значение сертификата)
YC-PG-CERTIFICATE=
# Логирование SQL-запросов
ENABLE_SQL_QUERY=true
# ----- Executors / Workers -----
# Включить AMQP-исполнитель (RabbitMQ). Требует секцию RabbitMQ ниже.
ENABLE_AMQP_EXECUTOR=0
# Включить Kubernetes-исполнитель (запуск задач как Job'ов). По умолчанию true.
ENABLE_KUBERNETES_EXECUTOR=1
COUNT_RUNNING_WORKERS=1
COUNT_CANCELING_WORKERS=1
COUNT_HANDLE_JOB_WORKERS=1
# ----- Kubernetes -----
# Namespace, в котором создаются Job'ы задач
JOBS_NAMESPACE=processing-testing
# Путь к kubeconfig. Если ПУСТО — используется in-cluster конфигурация.
KUBE_CONFIG=
# Контекст kubeconfig (для out-of-cluster)
KUBE_CONTEXT=
# Адрес API-сервера кластера (для out-of-cluster, с InsecureSkipTLSVerify)
KUBE_ADDR=
# ----- RabbitMQ (только при ENABLE_AMQP_EXECUTOR=1) -----
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=sarex
RABBITMQ_PASS=sarex
RABBITMQ_VHOST=/
RABBITMQ_USE_SSL=false
RABBITMQ_CERTIFICATE=
RABBITMQ_CREATE_EXCHANGE=autodesk.inputMessage
RABBITMQ_CANCEL_EXCHANGE=autodesk.cancelMessage
RABBITMQ_CREATE_ROUTING_KEY=converting
RABBITMQ_CANCEL_TOPIC=cancel
RABBITMQ_COMPLETENESS_EXCHANGE=autodesk.outputMessage
RABBITMQ_COMPLETENESS_TOPIC=output
# ----- Pooling / Workflows -----
# Лимит одновременно обрабатываемых workflow при выборке
MAX_WORKFLOWS_LIMIT=100
# Максимум одновременно выполняющихся Job'ов
MAX_RUNNING_JOBS=200
# Глубина выборки задач в часах (нужно для FIFO; 168 ч = 7 дней)
DEFAULT_GET_SINCE_LAST_HOURS=168
# Приоритет обрабатываемых workflow: default | high | low (задаёт «роль» инстанса)
WORKFLOW_PRIORITY=default
# ----- Планирование подов (NodeSelector / Tolerations / requests) -----
# CPU/Memory для high-resources / persistent
CPU_COUNT=5
MEMORY_GI=20
# CPU/Memory для low-resources
CPU_COUNT_LOW_RESOURCES=2
MEMORY_GI_LOW_RESOURCES=10
# CPU/Memory для super-high-resources
CPU_COUNT_HIGH_MEM=10
MEMORY_GI_HIGH_MEM=40
# Включить набор сервисов ресурсов (toleration/nodeselector)
ENABLE_TOLERATION=1
# Toleration/NodeSelector для high/low ресурсов
TOLERATION_KEY=dedicated
TOLERATION_VALUE=processing
# Toleration для super-high-resources
TOLERATION_KEY_HIGH_MEM=dedicated
TOLERATION_VALUE_HIGH_MEM=high-mem
# Toleration для persistent
TOLERATION_KEY_PERSISTENT=
TOLERATION_VALUE_PERSISTENT=
# Значения по умолчанию для всех подов
DEFAULT_TOLERATION_KEY=
DEFAULT_TOLERATION_VALUE=
DEFAULT_NODE_SELECTOR_KEY=
DEFAULT_NODE_SELECTOR_VALUE=
DEFAULT_IMAGE_PULL_POLICY=always
DEFAULT_CPU_REQUESTS=100m
DEFAULT_MEMORY_REQUESTS=64Mi
# ----- Хранилища и внешние сервисы (флаги ENABLE_*) -----
# Путь к service account JSON для S3 (в проде задаётся Helm'ом,
# напр. /etc/sarex/yc-s3/yc-s3-service-account.json)
S3_SERVICE_ACCOUNT=
ENABLE_S3_STORAGE=1
ENABLE_S3V2_STORAGE=1
ENABLE_PDM_STORAGE=1
ENABLE_URL_STORAGE=1
ENABLE_LOCAL=0
ENABLE_BIM_API_DB=1
ENABLE_BIM_API_CH=1
ENABLE_BIM_API_V2_DB=1
ENABLE_PDM_API_DB=1
ENABLE_COMPARISONS_API_DB=1
ENABLE_ISSUE_API_DB=0
ENABLE_RESOURCES_API=0
ENABLE_MAIL_GUN=1
ENABLE_SMTP=0
ENABLE_WORKSPACE_API_DB=0
ENABLE_CROSS_SECTION_API_DB=0
# Debug-флаги (также пробрасываются в под задачи)
BIM_API_DEBUG=0
COMPARISONS_API_DEBUG=0
# ----- URL'ы внешних сервисов (internal/external) -----
INTERNAL_PDM_URL=http://api-service.documentations-stage
EXTERNAL_PDM_URL=https://stage-api.sarex.io/documentations
INTERNAL_FILESTREAM_URL=http://filestream-service.documentations-stage
EXTERNAL_FILESTREAM_URL=https://stage-api.sarex.io/files
INTERNAL_REMARK_URL=http://remarks-service.remarks-stage
EXTERNAL_REMARK_URL=https://stage-api.sarex.io/remarks
INTERNAL_WORKSPACE_URL=http://workspaces-service.workspaces-stage
EXTERNAL_WORKSPACE_URL=https://stage-api.sarex.io/workspaces
DJANGO_HOST=https://stage.sarex.io
# ----- Security -----
# Запускать контейнеры задач под фиксированным UID
ENABLE_RUNASUSER=false
USER_RUNASUSER=1000
# ============================================================================
# Переменные ниже читаются НЕ конфигом engine, а напрямую через os.Getenv
# (pkg/kube_services/services.go) и пробрасываются в env запускаемых Job-подов
# при включённом соответствующем ENABLE_*-флаге. Значения — пути к JSON-конфигам
# внутри контейнера (в проде монтируются Helm'ом как секреты-volume).
# ============================================================================
BIM_API_DB=/etc/sarex/bim-api-db-stage.json
BIM_API_CH=/etc/sarex/bim-api-ch.json
BIM_API_CH_DEBUG=0
BIM_API_V2_DB=/etc/sarex/bim-api-v2-db-stage.json
BIM_API_V2_DEBUG=0
BIM_API_V2_DB_2=/etc/sarex/second-bim-bd/db.json
BIM_API_V2_DB_3=/etc/sarex/third-bim-bd/db.json
BIM_API_V2_DB_4=/etc/sarex/fourth-bim-bd/db.json
COMPARISONS_API_DB=/etc/comparisons/comparisons-db-stage.json
ISSUE_API_DB=/etc/issues/issues-db.json
ISSUE_API_DEBUG=0
RESOURCES_API_INTERNAL_HOST=
SMTP=/etc/smtp-secret/env.json
PDM_API_DB=/etc/pdm/pdm-api-db-stage.json
PDM_API_DEBUG=0
WORKSPACE_API_DB=/etc/ws/ws-api-db.json
WORKSPACE_API_DEBUG=0
CROSS_SECTION_API_DB=/etc/cross_section/db.json
CROSS_SECTION_API_DEBUG=0
# Прокидываются в под всегда (сервис Default)
WORKFLOWS_SENTRY_DSN=
WORKFLOWS_SENTRY_DEBUG=0
ENVIRONMENT=stage
# Mailgun-конфиг (при ENABLE_MAIL_GUN)
MAILGUN=/etc/mailgun-secret/env.json
# ============================================================================
# Переменные ниже присутствуют в config.env / Helm, но кодом engine НЕ читаются
# (легаси / задел). Оставлены для справки, включать не обязательно:
# API_ADDRESS — REST API отсутствует, реальный HTTP только pprof на :8081
# CONTROL_PLANE_PERIOD
# ENABLE_GOOGLE_STORAGE, GOOGLE_STORAGE_BUCKET, GOOGLE_STORAGE_PROJECT
# ENABLE_SRX_TMP
# MAX_TASKS_IN_PERIOD
# ENABLE_PDM_DB
# POD_NAME
# RABBITMQ_CREATE_TOPIC — в конфиге нет такого поля (см. RABBITMQ_CREATE_ROUTING_KEY)
# ============================================================================

View File

@ -0,0 +1,48 @@
# Эндпоинты, с которыми взаимодействует workflows-frontend
Микрофронтенд `workflows-frontend` обращается к внешним HTTP-сервисам через тонкий слой API, построенный поверх SDK `@sarex-team/sdk-js`. В отличие от `transmittal-frontend`, здесь нет реестра-объекта `endpoints` с полями `service/method/path/body/cache`. Вместо этого каждый вызов оформлен отдельной функцией-обёрткой (`fetch*`), которая напрямую вызывает `httpService.getRequest` / `httpService.postRequest` с указанием сервиса, URL и параметров. Все явные запросы фронтенда идут в один сервис — `workflows`. Хосты `sarex` и `zitadel` объявлены в конфигурации, но используются самим SDK (аутентификация через `AuthProvider`), а не прикладным кодом модуля.
## Как устроено взаимодействие
- **Слой API-функций**`module/shared/api/fetch/workflows.api.ts`. Здесь объявлены все обёртки над HTTP-запросами. Каждая функция принимает типизированные аргументы (`id`, `taskId`, `taskRunId`, `workflowId`, `ids`, пагинация, фильтры) и возвращает промис от `httpService`. Ключевые поля запроса:
- `service` — логическое имя сервиса (везде `"workflows"`), по которому SDK выбирает базовый хост;
- `url` — путь эндпоинта, собирается через шаблонные строки с подстановкой параметров;
- `data` — тело POST-запроса (например, `updates` при рестарте задачи);
- `axiosConfig.params` — query-параметры (`limit`, `offset`, `company_ids`).
- **Единая точка запроса**`httpService`, создаётся в `module/shared/api/http-service.ts` через `createHttpService({ axiosConfig, config, buildEnv, hosts })` из `@sarex-team/sdk-js`. Методы `getRequest`/`postRequest` этого сервиса — единственный способ выполнить внешний вызов. Под капотом SDK использует axios.
- **Выбор окружения и хоста** — окружение берётся из глобальной константы сборки `__BUILD_ENV__` (`const endpoint = (__BUILD_ENV__ as TypeEnvironment) || "prod"`). Для `local` дополнительно включается режим `setTypeOfHttpService("original")`. SDK по имени `service` и текущему `buildEnv` находит базовый хост в объекте `hosts` (`module/shared/api/hosts.ts`) и подставляет его перед `url`.
- **Реэкспорт**`module/shared/api/index.ts` реэкспортирует весь набор функций как `workflowsApi` и сам `httpService`.
- **Использование** — вызовы происходят из MobX-стора (`module/workflows/store/*.ts`: `workflows.ts`, `workflow.ts`, `task.ts`, `taskRun.ts`, `watchList.ts`).
- **Обработка ошибок** — отдельного файла `errors.ts` в проекте нет; маппинг и перехват ошибок HTTP выполняется внутри SDK `@sarex-team/sdk-js` и обрабатывается в сторах через try/catch с отображением состояний ошибки (`module/components/States/ErrorState.tsx`). Аутентификация и редиректы на страницу логина реализованы в `module/Auth/AuthProvider.tsx` через компонент `AuthProvider` из SDK.
## Базовые хосты по сервисам и окружениям
Источник: `module/shared/api/hosts.ts`. В коде определены окружения `local`, `stage`, `prod`, `preprod` и `cps` (закрытый контур Газпрома). Прикладной код обращается только к сервису `workflows`; хосты `sarex` и `zitadel` используются SDK для основного бэкенда и системы аутентификации (Zitadel).
| Сервис (service) | Назначение | local | stage | prod | preprod | cps (контур Газпром) |
| --- | --- | --- | --- | --- | --- | --- |
| workflows | API оркестрации воркфлоу и задач | `/sarex-workflows` | `https://stage-api.sarex.io/workflows` | `https://api.sarex.io/workflows` | `https://api.preprod.sarex.io/workflows` | `https://api.aeromonitoring.codm.gazprom.loc/workflows/` |
| sarex | Основной бэкенд Sarex (через SDK) | `/sarex-backend` | `/` | `/` | `/` | `https://aeromonitoring.codm.gazprom.loc/` |
| zitadel | Сервис аутентификации Zitadel (через SDK) | `/zitadel` | `https://idp.dev.stage.sarex.io` | `https://login.sarex.io` | `https://login.preprod.sarex.io` | — (не задан) |
Примечание: в окружении `local` пути относительные (проксируются через dev-сервер), в `stage/prod/preprod` — абсолютные URL. Для контура `cps` сервис `zitadel` не объявлен.
## Эндпоинты по сервисам
### workflows — оркестрация воркфлоу и задач
Все функции определены в `module/shared/api/fetch/workflows.api.ts`, `service: "workflows"`.
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| fetchGetWorkflows | GET | `/api/v1/workflows?limit={limit}&offset={offset}&company_ids={companyIds}` | Список воркфлоу с пагинацией и фильтром по компаниям (`company_ids` — id через запятую) |
| fetchGetWorkflowById | GET | `/api/v1/workflows/{id}` | Получить один воркфлоу по идентификатору |
| fetchGetWorkflowsStates | GET | `/api/v1/workflows/{ids}/state` | Получить состояния набора воркфлоу (`ids` — идентификаторы через запятую), используется для отслеживаемого списка |
| fetchGetWorkflowPosition | GET | `/api/v1/workflows/{workflowId}/position` | Позиция воркфлоу в очереди обработки |
| fetchPrioritizeWorkflow | POST | `/api/v1/workflows/{workflowId}/prioritize` | Повысить приоритет воркфлоу |
| fetchGetLogs | GET | `/api/v1/tasks-runs/{taskRunId}/logs` | Получить логи конкретного запуска задачи |
| fetchCancelTaskRun | POST | `/api/v1/tasks-runs/{taskRunId}/cancel` | Отменить запуск задачи |
| fetchRestartTask | POST | `/api/v1/tasks/{taskId}/restart` | Перезапустить задачу; тело запроса — объект `updates` (изменённые параметры/объекты/сервис-реквесты) |
| fetchMoveTaskToSuperHighResources | POST | `/api/v1/tasks/{taskId}/move_to_super_high_resources` | Перевести задачу на пул сверхвысоких ресурсов |
> Примечание: фронтенд вызывает `GET /api/v1/tasks-runs/{taskRunId}/logs` (`fetchGetLogs`), однако в текущем коде `workflows-api` соответствующий обработчик отсутствует (см. `workflows-api.openapi.yaml`, раздел «Замечания»). Это расхождение стоит проверить: либо эндпоинт реализуется другим сервисом/ingress, либо один из репозиториев устарел.

View File

@ -0,0 +1,133 @@
# Конфигурация проекта projects-frontend
Документ описывает, как конфигурируется микрофронтенд `projects-frontend`: переменные сборки, способы запуска, параметры Docker/nginx, Helm-чарта и CI.
## Способы конфигурирования
`projects-frontend` — это клиентский микрофронтенд (React 17 + MobX, сборка Webpack 5, Module Federation). У приложения **нет runtime-конфигурации и файла `.env`**: всё поведение, зависящее от окружения, определяется **на этапе сборки** одной переменной `BUILD_ENV`.
Разбор выполняется в `configWebpack/config/build.config.ts` (`extractBuildOptions`): значение `process.env.BUILD_ENV` (одно из `local` / `stage` / `prod` / `preprod` / `contour`, по умолчанию `prod`) определяет режим сборки (`development`/`production`) и «endpoint». Полученный `endPoint` через `DefinePlugin` (`configWebpack/buildPlugins.ts`) подставляется в бандл как глобальная константа `__ENDPOINT__`:
```js
new DefinePlugin({ __ENDPOINT__: JSON.stringify(endPoint) })
```
Константа `__ENDPOINT__` используется в рантайме бандла для выбора:
- карты хостов API — `src/shared/api/http-service.ts` (`const endpoint = (__ENDPOINT__ as TypeEnvironment) || "prod"`) поверх `src/shared/api/hosts.ts`;
- URL удалённого модуля timeline — `src/widgets/remote-timeline/ui/timelineProxy.tsx`;
- локального провайдера разработки — `src/app/bootstrap.tsx` (`__ENDPOINT__ === "local" ? <LocalDevProvider /> : <div />`).
Отдельного конфиг-файла (yaml/env) у приложения нет.
## Переменные сборки и запуска
| Переменная | Где используется | Значение по умолчанию | Назначение |
| --- | --- | --- | --- |
| `BUILD_ENV` | `webpack.config.ts`, `configWebpack/config/build.config.ts`, `Dockerfile` (build-arg) | `prod` | Целевое окружение сборки: `local`/`stage`/`prod`/`preprod`/`contour`. Определяет режим (`development`/`production`), карту хостов и URL удалённых модулей |
| `NPM_NEXUS_TOKEN` | `.npmrc`, `Dockerfile` (build-arg) | — | Токен доступа к приватному npm-реестру Nexus (`https://nexus.infra.sarex.io/repository/npm/`) для установки пакетов `@sarex-team/*` |
| `PORT` | `webpack.config.ts` | `9001` | Порт dev-сервера Webpack |
Версия Node фиксирована в `.nvmrc``v16.0.0`. Приватный реестр и авторизация заданы в `.npmrc`:
```
@sarex-team:registry=https://nexus.infra.sarex.io/repository/npm/
//nexus.infra.sarex.io/repository/npm/:_authToken=${NPM_NEXUS_TOKEN}
```
### npm-скрипты (`package.json`)
| Команда | Действие |
| --- | --- |
| `npm run dev` | Локальная разработка: `BUILD_ENV=local webpack serve --config webpack.config.ts` |
| `npm run build-module` | Сборка модуля: `webpack --config webpack.config.ts` (окружение — из `BUILD_ENV`) |
| `npm run build:start` | Раздача собранного бандла: `serve -s ./dist -l 9001` |
| `npm run lint` | Форматирование Prettier: `npx prettier --write .` |
## Сборка Webpack и Module Federation
Точка входа — `src/app/index.ts` (`webpack.config.ts`). Для всех окружений, кроме `local`, добавляется `ModuleFederationPlugin`:
- `name`: `srx_projects`;
- `filename`: `module/remoteEntry.js`;
- `exposes`: `./ProjectsPage``./src/app/App.tsx`;
- `shared` (singleton, `requiredVersion: false`): `react`, `react-dom`, `@material-ui/core`, `@sarex-team/sdk-js`.
Плагины (`configWebpack/buildPlugins.ts`): `HtmlWebpackPlugin`, `DefinePlugin`. В режиме `local` дополнительно `ProgressPlugin`, `ForkTsCheckerWebpackPlugin`, `ReactRefreshWebpackPlugin`; вне `local``MiniCssExtractPlugin` (хеши в именах файлов).
### Dev-сервер (`configWebpack/buildDevServer.ts`)
HTTPS, порт `9001`, `historyApiFallback`, `hot`. Прокси на stage-окружение с `pathRewrite`:
| Префикс | Target |
| --- | --- |
| `/sarex-backend` | `https://stage.sarex.io` |
| `/sarex-gateway` | `https://stage-api.sarex.io/gateway` |
| `/sarex-documentations` | `https://stage-api.sarex.io/documentations` |
| `/sarex-api` | `https://stage-api.sarex.io` |
Заголовки CORS dev-сервера разрешают любой origin, методы `GET, POST, PUT, DELETE, PATCH, OPTIONS` и заголовки `X-Requested-With, content-type, Authorization`.
## Docker
Многоступенчатая сборка (`Dockerfile`):
1. **Стадия сборки** (`node:16`): установка зависимостей (`npm i` с `NPM_NEXUS_TOKEN`), `npm run lint`, `BUILD_ENV=$BUILD_ENV npm run build-module``dist`.
2. **Стадия раздачи** (`nginx:1.19.6`): копирование `dist` в `/dist` и `nginx/nginx.conf` в `/etc/nginx/nginx.conf`.
Build-args: `BUILD_ENV`, `NPM_NEXUS_TOKEN`.
### nginx (`nginx/nginx.conf`)
Статика раздаётся с `root /dist` на порту `80`. Особенности:
- `location = /ping` → возвращает `200 {"result": "ok"}` (используется как liveness/readiness-проба);
- `location = /module/remoteEntry.js``Cache-Control: no-store, no-cache, must-revalidate…`, `expires off` (точка входа Module Federation не кешируется);
- `gzip on`, логи в `stdout`/`stderr`.
## Helm-чарт (`.helm`)
Чарт `projects-frontend` (`Chart.yaml`, `type: application`, `version: 0.1.0`, `appVersion: 1.16.0`) разворачивает статику как `Deployment` + `Service` (`templates/static.yaml`) и публикует её через Istio `VirtualService` (`templates/mesh-config.yaml`).
Значения задаются в `values-<env>.yaml` (блок `static`):
| Параметр | `stage` | `preprod` | `production` |
| --- | --- | --- | --- |
| `static.host` | `stage-modules.sarex.io` | `modules.preprod.sarex.io` | `modules.sarex.io` |
| `static.replicas` | `1` | `2` | `2` |
| `static.path` | `/projects/static/` | `/projects/static/` | `/projects/static/` |
| `static.image` | `sarex/projects-frontend-static:latest` | то же | то же |
| `static.port` / `static.service_port` | `80` / `80` | `80` / `80` | `80` / `80` |
| `static.requests` | `memory: 100Mi`, `cpu: 100m` | то же | то же |
| `imagePullSecrets` | `dockerhub` | `dockerhub` | `dockerhub` |
Общее для всех окружений: `static.name: projects-frontend-static`, `static.service_name: projects-frontend-static-service`, `static.version: stable`. Пробы `livenessProbe`/`readinessProbe` бьют в `/ping` (`templates/static.yaml`).
`VirtualService` (`templates/mesh-config.yaml`): хост `static.host`, шлюз `gateway/modules-gateway`, матч по префиксу `static.path` (`/projects/static/`) с `rewrite` на `/`. Политика CORS: `allowOrigins` по регулярке `(https://.*\.sarex\.io)|(https://localhost:.*)`, `allowMethods: [GET, POST, PUT, PATCH, HEAD, DELETE]`, `allowHeaders: [Authorization, Content-Type]`, `maxAge: 24h`.
## CI/CD (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны из `generic/common-ci` (`universal-pipeline-*`, `common-security-scan`, `common-build`) и переключает окружение по ветке/тегу через `workflow.rules`:
| Условие | STAND | Namespace | `BUILD_ENV` |
| --- | --- | --- | --- |
| ветка `master` | `preprod` | `projects-preprod` | `preprod` |
| ветка `stage` | `stage` | `projects-stage` | `stage` |
| тег (`CI_COMMIT_TAG`) | `prod` | `projects-prod` | `prod` |
Ключевые переменные пайплайна (общие для всех окружений): `RELEASE_NAME`/`CHART_NAME` = `projects-frontend`, `IMAGE_PATH` = `static.image`, `HELM_SET_ARGS` = `--set static.image=${IMAGE_NAME}`, `BUILD_ARGS` = `--build-arg BUILD_ENV=<env> --build-arg NPM_NEXUS_TOKEN=${NPM_NEXUS_TOKEN}`, `DOCKERFILE_PATH` = `Dockerfile`. Флаги этапов: `ENABLE_LINTER` (`false`), `ENABLE_BUILD_CHART`, `ENABLE_BUILD_IMAGE`, `ENABLE_STATE_UPDATE`, `ENABLE_DEPLOY` (`true`). `CHART_VERSION` задаётся как `0.0.1-<env>`. Стадии: `linter → test → unittest → prebuild-secscan → build → state-update → deploy`.
## Замечания и потенциальные проблемы
- **Нет runtime-конфигурации.** Всё, что зависит от окружения, «зашивается» в бандл на этапе сборки через `BUILD_ENV`/`__ENDPOINT__`. Пересборка под другое окружение обязательна — переопределить хосты в рантайме нельзя.
- **Значение по умолчанию `BUILD_ENV=prod`.** Если переменная не задана при сборке, собирается production-вариант (`build.config.ts`, `webpack.config.ts`, `http-service.ts`). Для локальной разработки нужно явно `BUILD_ENV=local` (скрипт `npm run dev` это делает).
- **Окружение `contour` неполно сконфигурировано.** Оно перечислено в `build.config.ts` и типах, но в `src/shared/api/hosts.ts` карта хостов для `contour` отсутствует, а URL удалённого модуля timeline для `contour` пуст (`timelineProxy.tsx`). Сборка с `BUILD_ENV=contour` не сможет разрешить хосты API.
- **README не совпадает с `package.json`.** В `README.md` указаны `nvm use 16.0.0` и `npm run start:local`, однако скрипта `start:local` в `package.json` нет — локальный запуск выполняется командой `npm run dev`. `.nvmrc` фиксирует `v16.0.0`.
- **Версии чарта расходятся.** В `Chart.yaml``version: 0.1.0` / `appVersion: 1.16.0`, тогда как в CI `CHART_VERSION` задаётся как `0.0.1-<env>`. Значения не синхронизированы.
- **Приватный реестр.** Установка зависимостей требует валидный `NPM_NEXUS_TOKEN` (`.npmrc`); без него `npm i` (в т.ч. в Docker-сборке) завершится ошибкой авторизации.
## Минимальный набор для локального запуска
1. `nvm use` (Node `v16.0.0` из `.nvmrc`).
2. Экспортировать `NPM_NEXUS_TOKEN` (доступ к Nexus) и выполнить `npm i`.
3. `npm run dev` — соберёт с `BUILD_ENV=local` и поднимет HTTPS dev-сервер на `https://localhost:9001` (запросы к backend проксируются на stage через `/sarex-backend`, `/sarex-gateway`, `/sarex-documentations`, `/sarex-api`).

View File

@ -0,0 +1,94 @@
# Эндпоинты, с которыми взаимодействует projects-frontend
Документ описывает все HTTP-эндпоинты внешних сервисов, к которым обращается модуль (микрофронтенд `projects-frontend`), а также удалённые модули, которые он подключает и экспортирует через Module Federation.
## Как устроено взаимодействие
Запросы описаны в слое `src/shared/api`. Функции запросов сгруппированы по доменам в каталоге `src/shared/api/fetch/*.api.ts` и реэкспортируются из `src/shared/api/index.ts` (`targetsApi`, `resourcesApi`, `projectsApi`, `userApi`, `companyApi`).
Каждый запрос выполняется через единый `httpService` (`src/shared/api/http-service.ts`), созданный фабрикой `createHttpService` из `@sarex-team/sdk-js` (поверх `axios`). У сервиса есть методы `getRequest` / `postRequest` / `putRequest` / `deleteRequest`, принимающие объект с полями:
- `service` — логическое имя сервиса (см. таблицу хостов ниже);
- `url` — путь запроса (относительно базового хоста сервиса);
- `data` — тело запроса (для POST/PUT);
- `axiosConfig.params` — query-параметры;
- `isCSRF` — признак необходимости передать CSRF-токен (для сервиса `sarex`);
- `cache`, `queryOptions`, `queryKey` — опции кеширования и ключи кеша.
Базовый хост подставляется по `service` из карты хостов `src/shared/api/hosts.ts` в зависимости от окружения сборки. Окружение задаётся значением `__ENDPOINT__`, которое подставляется на этапе сборки Webpack (`DefinePlugin`) из переменной `BUILD_ENV` и по умолчанию равно `prod` (`const endpoint = (__ENDPOINT__ as TypeEnvironment) || "prod"`).
## Базовые хосты по сервисам и окружениям
Значения из `src/shared/api/hosts.ts`. Итоговый URL = `<базовый хост сервиса>` + `url` эндпоинта.
| Сервис (`service`) | Назначение | `local` | `stage` | `prod` |
| --- | --- | --- | --- | --- |
| `sarex` | Основной backend (core, pm) | `/sarex-backend` (прокси dev-сервера → `https://stage.sarex.io`) | `/` (относительные пути, тот же origin) | `/` |
| `gateway` | Gateway/API Sarex | `/sarex-gateway` (прокси → `https://stage-api.sarex.io/gateway`) | `https://stage-api.sarex.io/gateway` | `https://api.sarex.io/gateway` |
| `documentations` | Сервис документации | `/sarex-documentations` (прокси → `https://stage-api.sarex.io/documentations`) | `https://stage-api.sarex.io/documentations` | `https://api.sarex.io/documentations` |
| `sarexApi` | API Sarex (корень) | `/sarex-api` (прокси → `https://stage-api.sarex.io`) | `https://stage-api.sarex.io` | `https://api.sarex.io` |
| `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` |
| `eavV1` | EAV (атрибуты) | — | `https://stage-api.sarex.io/eav` | `https://api.sarex.io/eav` |
| `notifications` | Сервис уведомлений (lambdas) | — | `https://stage-api.sarex.io/lambdas/notification` | `https://api.sarex.io/lambdas/notification` |
| `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://idp.dev.stage.sarex.io` | `https://login.sarex.io` |
> Также определено окружение `preprod` (`https://api.preprod.sarex.io/*`, `zitadel``https://login.preprod.sarex.io`). В `local` часть сервисов проксируется dev-сервером Webpack (`configWebpack/buildDevServer.ts`): `/sarex-backend`, `/sarex-gateway`, `/sarex-documentations`, `/sarex-api`. Остальные сервисы в `local` заданы пустой строкой (относительные пути). Окружение `contour` присутствует в конфигурации сборки (`build.config.ts`), но в `hosts.ts` для него карта хостов не задана.
## Эндпоинты по сервисам
Реально вызываются эндпоинты двух сервисов — `sarex` и `gateway`.
### `sarex` — Основной backend (core, pm)
Все запросы к `sarex` выполняются с `isCSRF: true`.
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchTargetLinks` | GET | `/api/core/target-links/` | Список ссылок таргета (кешируется, `stateTime: 20`) |
| `fetchCreateTargetLink` | POST | `/api/core/target-links/` | Создать ссылку (`name`, `link`, `target`, `type`) |
| `fetchUpdateTargetLink` | PUT | `/api/core/target-links/{id}/` | Обновить ссылку |
| `fetchDeleteTargetLink` | DELETE | `/api/core/target-links/{id}/` | Удалить ссылку |
| `fetchMilestonesByProjectId` | GET | `/api/pm/msp/projects/{projectId}/key_milestones/` | Ключевые вехи проекта |
| `fetchUsersByCompanyId` | GET | `/api/core/users/?companies={companyId}&limit=10000&id={usersIds}` | Пользователи компании по списку id |
| `fetchCompanyById` | GET | `/api/core/companies/{companyId}/` | Компания по id (кешируется) |
### `gateway` — Gateway/API Sarex
| Функция | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `fetchResources` | GET | `/api/v1/resources?company_id={companyId}` | Список ресурсов компании (кешируется, `stateTime: 20`) |
| `fetchProjectsByCompanyId` | GET | `/api/v2/resources?company_id={companyId}&limit=10000` | Проекты компании (ресурсы v2, кешируется) |
## Удалённые модули (Module Federation)
Помимо HTTP-запросов, модуль взаимодействует с другими микрофронтендами через Webpack Module Federation.
### Подключаемый удалённый модуль
`src/widgets/remote-timeline` динамически загружает удалённый модуль `srx_pm`, экспонированный модуль `./Timeline` (`src/widgets/remote-timeline/ui/timelineProxy.tsx`, загрузка через `src/widgets/remote-timeline/lib/loader.ts`).
| Окружение | URL `remoteEntry.js` |
| --- | --- |
| `stage` | `https://stage-modules.sarex.io/pm/module/remoteEntry.js` |
| `local` | `https://stage-modules.sarex.io/pm/module/remoteEntry.js` |
| `prod` | `https://modules.sarex.io/pm/module/remoteEntry.js` |
| `preprod` | `https://modules.sarex.io/pm/module/remoteEntry.js` |
| `contour` | `""` (не задан) |
### Экспортируемый модуль
Сам `projects-frontend` при сборке (для всех окружений, кроме `local`) публикует себя как удалённый модуль `srx_projects` (`webpack.config.ts`, `ModuleFederationPlugin`):
- `filename`: `module/remoteEntry.js`;
- `exposes`: `./ProjectsPage``./src/app/App.tsx`;
- `shared` (singleton): `react`, `react-dom`, `@material-ui/core`, `@sarex-team/sdk-js`.
## Обработка ошибок
Отдельного модуля маппинга ошибок в `projects-frontend` нет (в отличие от некоторых других фронтендов) — обработка ответов и ошибок делегирована `httpService` из `@sarex-team/sdk-js`. Заголовки CORS для запросов в режиме разработки задаются dev-сервером Webpack, а в кластере — политикой CORS Istio `VirtualService` (`.helm/templates/mesh-config.yaml`): разрешённые источники по регулярке `(https://.*\.sarex\.io)|(https://localhost:.*)`, методы `GET, POST, PUT, PATCH, HEAD, DELETE`, заголовки `Authorization`, `Content-Type`.