diff --git a/apps/mapper/.env.example b/apps/mapper/.env.example new file mode 100644 index 0000000..68c57f2 --- /dev/null +++ b/apps/mapper/.env.example @@ -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-клиентов +# берётся из _TIMEOUT (по умолчанию 30). См. CONFIGURATION.md. diff --git a/apps/mapper/CONFIGURATION.md b/apps/mapper/CONFIGURATION.md new file mode 100644 index 0000000..0483a67 --- /dev/null +++ b/apps/mapper/CONFIGURATION.md @@ -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`), поэтому у каждого из них одинаковый набор из трёх переменных: `_HOST`, `_TIMEOUT`, `_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`. Чтобы поднять таймаут, задавайте `_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`. diff --git a/apps/mapper/ENDPOINTS.md b/apps/mapper/ENDPOINTS.md new file mode 100644 index 0000000..a1c9785 --- /dev/null +++ b/apps/mapper/ENDPOINTS.md @@ -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 = `` + путь ниже. Значения по окружениям — из `.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: ` — обязателен для обоих эндпоинтов; пробрасывается во все внешние запросы как есть. +- `Identity: ` — опционален; при наличии включается режим 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-эндпоинта у сервиса нет. diff --git a/apps/mapper/openapi.yaml b/apps/mapper/openapi.yaml new file mode 100644 index 0000000..344e6c5 --- /dev/null +++ b/apps/mapper/openapi.yaml @@ -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" diff --git a/apps/message-hub/.env.example b/apps/message-hub/.env.example new file mode 100644 index 0000000..8b128c0 --- /dev/null +++ b/apps/message-hub/.env.example @@ -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 diff --git a/apps/message-hub/CONFIGURATION.md b/apps/message-hub/CONFIGURATION.md new file mode 100644 index 0000000..c10584b --- /dev/null +++ b/apps/message-hub/CONFIGURATION.md @@ -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` рядом с этим документом. diff --git a/apps/message-hub/ENDPOINTS.md b/apps/message-hub/ENDPOINTS.md new file mode 100644 index 0000000..898f3ef --- /dev/null +++ b/apps/message-hub/ENDPOINTS.md @@ -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 = `` + путь из таблицы. + +| Сервис (`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) | Файловое хранилище | diff --git a/apps/notes/.env.example b/apps/notes/.env.example new file mode 100644 index 0000000..dc7110a --- /dev/null +++ b/apps/notes/.env.example @@ -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 diff --git a/apps/notes/CONFIGURATION.md b/apps/notes/CONFIGURATION.md new file mode 100644 index 0000000..81ea848 --- /dev/null +++ b/apps/notes/CONFIGURATION.md @@ -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://:@:/`. + +### 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`). diff --git a/apps/notes/ENDPOINTS.md b/apps/notes/ENDPOINTS.md new file mode 100644 index 0000000..cfeb3a5 --- /dev/null +++ b/apps/notes/ENDPOINTS.md @@ -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`. diff --git a/apps/notes/openapi.yaml b/apps/notes/openapi.yaml new file mode 100644 index 0000000..9477b70 --- /dev/null +++ b/apps/notes/openapi.yaml @@ -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 } diff --git a/apps/pm/.env.example b/apps/pm/.env.example new file mode 100644 index 0000000..a7d854d --- /dev/null +++ b/apps/pm/.env.example @@ -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 diff --git a/apps/pm/CONFIGURATION.md b/apps/pm/CONFIGURATION.md new file mode 100644 index 0000000..7ffd5b9 --- /dev/null +++ b/apps/pm/CONFIGURATION.md @@ -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_`; +- **вложенного делимитера нет** — каждая настройка задаётся плоской переменной вида ``, напр. `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` | + +Для каждого клиента доступны переменные `HOST`, `API_PREFIX`, `INTERNAL_HOST`, `INTERNAL_PREFIX`, `TIMEOUT`, `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`. diff --git a/apps/pm/ENDPOINTS.md b/apps/pm/ENDPOINTS.md new file mode 100644 index 0000000..2a7a8ff --- /dev/null +++ b/apps/pm/ENDPOINTS.md @@ -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.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` (напр. «Некорректные данные»). diff --git a/apps/pm/openapi.yaml b/apps/pm/openapi.yaml new file mode 100644 index 0000000..2f84b2c --- /dev/null +++ b/apps/pm/openapi.yaml @@ -0,0 +1,1768 @@ +openapi: 3.0.3 + +info: + title: PM Backend API + version: "1.0.0" + description: | + REST API сервиса **pm-backend** (`planning/pm-backend`) — управление + проектами и папками, задачами (иерархия, связи, ресурсы), базовыми планами + (состояниями), атрибутами, календарями, настройками КСГ, детализацией задач, + системным журналом и экспортом. + + Сервис написан на Python (**Django 5.1 + Django REST Framework**) и запускается + как ASGI-приложение (`config.asgi_root:application`) через gunicorn с воркерами + `uvicorn.workers.UvicornWorker` (порт `8000`). Роутинг задан в `config/urls.py` + и состоит из трёх групп: + + - публичный API — префикс `/api/pm/msp/` (`sarex.pm.urls`); + - внешний API v2 — префикс `/api/pm/external/v2/` (`sarex.pm.urls_external`), + только чтение; + - внутренний API — префикс `/internal/pm/` (`sarex.pm.urls_internal`), + предназначен для вызовов внутри кластера, аутентификация не требуется. + + ### Аутентификация + Публичные (`/api/pm/msp/*`) и внешние (`/api/pm/external/v2/*`) эндпоинты + требуют аутентификации (DRF `IsAuthenticated`). Проверка выполняется по цепочке + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`): + + 1. **Zitadel** (`ZitadelJWTAuthentication`) — требуются одновременно заголовки + `Authorization: Bearer ` и `Identity: Identity `. При отсутствии + любого из них — переход к следующему механизму/ошибка. + 2. **sarex-backend** (`JWTAuthentication`) — подпись токена `Authorization: + Bearer ` проверяется публичным RSA-ключом (`AUTH_PUBLIC_KEY`, + алгоритм `RS512`). При `SERVER_DEBUG=true` аутентификация обходится + (возвращается анонимный пользователь). + + Внутренние эндпоинты (`/internal/pm/*`) имеют `authentication_classes=[]` и + `AllowAny` — доступ ограничивается сетевым слоем. Отдельный публичный эндпоинт + `/api/pm/msp/external-task-info/` также открыт (`AllowAny`). + + ### Пагинация + По умолчанию используется DRF `LimitOffsetPagination` (`PAGE_SIZE=1000`). + Списочные ответы оборачиваются в объект `{ count, next, previous, results }`; + размер страницы задаётся query-параметром `limit`, смещение — `offset`. Часть + «тяжёлых» списков (задачи, привязки ресурсов, связи задач) использует + `ProjectTaskPagination` (`default_limit=30000`). + + ### Обработка ошибок + Ошибки возвращаются в стандартном формате DRF: ошибки валидации — `400` + (`{ "": [""] }` либо `{ "detail": "..." }`), нет прав — `403` + (`{ "detail": "..." }`), не аутентифицирован — `401`, не найдено — `404`. + Объектные права проверяются `PermissionMixin` (raw-SQL), суперпользователь + их обходит. + + ### Замечания (расхождения кода) + - Внешние вьюсеты v2 объявляют `http_method_names = ['get']`, поэтому доступны + только `list`/`retrieve` (и GET-`@action`), даже если в коде есть методы + записи. + - Ряд bulk-эндпоинтов принимает JSON-массив (list-сериализатор). + - Datetime-поля (`DateTimeWithoutTZFiled`) наивные — таймзона отбрасывается. + - Внутренние эндпоинты принимают/возвращают «сырые» словари, без модельных + сериализаторов. + + contact: + name: pm-backend + url: https://gitlab/planning/pm-backend + +servers: + - url: https://api.sarex.io + description: Production (ingress) + - url: https://stage-api.sarex.io + description: Stage (ingress) + - url: http://pm-backend-service.planning.svc.cluster.local:8000 + description: Внутрикластерный адрес (ClusterIP, порт 8000) — единственный способ достучаться до /internal/pm + - url: http://localhost:8000 + description: Локальный запуск (gunicorn/uvicorn, порт 8000) + +tags: + - name: projects + description: Проекты и папки — CRUD, копирование, экспорт, права, шаблоны + - name: tasks + description: Задачи проекта — CRUD, массовые операции, права, описания + - name: resources + description: Ресурсы и их привязки к задачам и элементам + - name: states + description: Состояния проекта (базовые планы) + - name: relations + description: Связи задач и проектов, атрибутивные связи + - name: attributes + description: Атрибуты проекта и значения атрибутов задач + - name: calendars + description: Календари + - name: settings + description: Настройки/состояния КСГ + - name: detailing + description: Детализация задач, визуальные профили + - name: comments + description: Комментарии к задачам + - name: system-log + description: Системный журнал и откаты + - name: external + description: Внешний API v2 (только чтение) + - name: internal + description: Внутренние эндпоинты (только внутри кластера, без аутентификации) + +security: + - bearerAuth: [] + +paths: + # ========================================================================== + # Projects (/api/pm/msp/projects) + # ========================================================================== + /api/pm/msp/projects/: + get: + tags: [projects] + summary: Список проектов и папок + operationId: listProjects + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: search, in: query, schema: { type: string } } + - { name: ids, in: query, description: CSV идентификаторов, schema: { type: string } } + - { name: resource_id, in: query, schema: { type: string } } + - { name: company_id, in: query, schema: { type: integer } } + - { name: parent_id, in: query, schema: { type: integer } } + - { name: only_children, in: query, schema: { type: boolean } } + - { name: templates, in: query, schema: { type: boolean } } + - { name: is_folder, in: query, schema: { type: boolean } } + - { name: strict, in: query, schema: { type: boolean } } + - { name: extend, in: query, schema: { type: boolean } } + - { name: schema, in: query, description: "напр. tiny/detail", schema: { type: string } } + responses: + '200': + description: Страница проектов + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Project' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + post: + tags: [projects] + summary: Создать проект/папку + operationId: createProject + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ProjectCreate' } + responses: + '201': + description: Проект создан + content: + application/json: + schema: { $ref: '#/components/schemas/Project' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + + /api/pm/msp/projects/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [projects] + summary: Проект по id + operationId: retrieveProject + parameters: + - { name: extend, in: query, schema: { type: boolean } } + - { name: schema, in: query, schema: { type: string } } + responses: + '200': + description: Проект + content: + application/json: + schema: { $ref: '#/components/schemas/Project' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [projects] + summary: Полное обновление проекта + operationId: updateProject + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [projects] + summary: Частичное обновление проекта + operationId: partialUpdateProject + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [projects] + summary: Удалить проект + operationId: deleteProject + responses: + '204': { description: Удалено } + '404': { $ref: '#/components/responses/NotFound' } + + /api/pm/msp/projects/root_folder/: + post: + tags: [projects] + summary: Создать корневую папку + operationId: createRootFolder + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/FolderCreate' } } } + responses: + '201': { description: Папка создана, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/rename/: + post: + tags: [projects] + summary: Переименовать проект + operationId: renameProject + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/import/: + post: + tags: [projects] + summary: Импорт задач в проект + operationId: importProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + pk: { type: integer } + from_type: { type: string, enum: [sarex, ms_project, primavera, template] } + update: { type: boolean } + template_id: { type: integer, nullable: true } + required: [pk, from_type] + responses: + '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } } + '404': { $ref: '#/components/responses/NotFound' } + + /api/pm/msp/projects/import-from-gasprom/: + post: + tags: [projects] + summary: Импорт проекта из Газпром ЦПС + operationId: importProjectFromGaspromCps + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_name: { type: string } + company_id: { type: integer } + file_url: { type: string } + required: [project_name, company_id, file_url] + responses: + '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/states/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Состояния (базовые планы) проекта + operationId: listProjectStates + responses: + '200': { description: Список состояний, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } + + /api/pm/msp/projects/{id}/permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Права проекта + operationId: getProjectPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + post: + tags: [projects] + summary: Сохранить права проекта + operationId: saveProjectPermissions + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/all_permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Все права проекта + operationId: getProjectAllPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/copy_project/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Копировать проект + operationId: copyProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + parent_id: { type: integer, nullable: true } + project_name: { type: string } + assets_mapping: { type: object } + with_resources: { type: boolean } + responses: + '201': { description: Проект скопирован, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/create_template/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Создать шаблон из проекта + operationId: createTemplateFromProject + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_name: { type: string } + description: { type: string, nullable: true } + required: [project_name] + responses: + '201': { description: Шаблон создан, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } } + + /api/pm/msp/projects/{id}/export_project_to_pdf/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Экспорт проекта в PDF + operationId: exportProjectToPdf + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ExportProject' } } } + responses: + '200': { description: Задача экспорта запущена } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/project_export/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Экспорт проекта (xlsx/xml) + operationId: projectExport + responses: + '200': { description: Экспорт запущен } + + /api/pm/msp/projects/{id}/key_milestones/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Ключевые вехи проекта + operationId: getKeyMilestones + responses: + '200': { description: Вехи, content: { application/json: { schema: { type: array, items: { type: object } } } } } + + /api/pm/msp/projects/{id}/profiles/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Визуальные профили проекта + operationId: getProjectProfiles + responses: + '200': { description: Профили, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } } + + /api/pm/msp/projects/{id}/time_markers_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Данные временных маркеров + operationId: getTimeMarkersData + parameters: + - { name: attributes, in: query, description: CSV идентификаторов атрибутов, schema: { type: string } } + - { name: with_hierarchy, in: query, schema: { type: boolean } } + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/relations/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Связи/интеграции проекта + operationId: getProjectRelations + responses: + '200': { description: Связи, content: { application/json: { schema: { type: array, items: { type: object } } } } } + + /api/pm/msp/projects/{id}/bulk_integration/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Массовое создание интеграций + operationId: bulkIntegration + responses: + '200': { description: OK } + + /api/pm/msp/projects/{id}/issues_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Данные проблем проекта + operationId: getIssuesData + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + + /api/pm/msp/projects/{id}/formula/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [projects] + summary: Пересчёт по формуле + operationId: applyFormula + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/rule/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [detailing] + summary: Правило детализации проекта + operationId: getProjectRule + responses: + '200': { description: Правило, content: { application/json: { schema: { type: object } } } } + post: + tags: [detailing] + summary: Создать правило детализации + operationId: createProjectRule + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + attributes: { type: array, items: { type: integer } } + summary_fields: { type: array, items: { type: string } } + mode: { type: string } + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [projects] + summary: Задачи проекта + operationId: getProjectTasks + responses: + '200': { description: Задачи, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } + + /api/pm/msp/projects/{id}/create_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [tasks] + summary: Массовое создание задач + operationId: bulkCreateTasks + requestBody: + required: true + content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/update_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [tasks] + summary: Массовое обновление задач + operationId: bulkUpdateTasks + requestBody: + required: true + content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskUpdate' } } } } + responses: + '200': { description: Обновлено } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/projects/{id}/delete_tasks/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + delete: + tags: [tasks] + summary: Массовое удаление задач + operationId: bulkDeleteTasks + requestBody: + required: true + content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } + responses: + '204': { description: Удалено } + + /api/pm/msp/projects/{id}/system-logs/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [system-log] + summary: Системные логи проекта + operationId: getSystemLogs + parameters: + - { name: task_id, in: query, schema: { type: integer } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Страница логов + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: { results: { type: array, items: { $ref: '#/components/schemas/SystemLog' } } } + + /api/pm/msp/projects/{id}/system-log-detail/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [system-log] + summary: Деталь записи журнала + operationId: getSystemLogDetail + parameters: + - { name: log_id, in: query, required: true, schema: { type: integer } } + responses: + '200': { description: Деталь, content: { application/json: { schema: { $ref: '#/components/schemas/SystemLog' } } } } + + /api/pm/msp/projects/{id}/rollback-to-record/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [system-log] + summary: Откат к записи журнала + operationId: rollbackToRecord + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + log_id: { type: integer } + read_only: { type: boolean } + required: [log_id] + responses: + '200': { description: OK } + '400': { $ref: '#/components/responses/BadRequest' } + + # ========================================================================== + # Tasks (/api/pm/msp/tasks) + # ========================================================================== + /api/pm/msp/tasks/: + get: + tags: [tasks] + summary: Список задач + operationId: listTasks + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: project, in: query, schema: { type: integer } } + - { name: ids, in: query, schema: { type: string } } + - { name: name__icontains, in: query, schema: { type: string } } + - { name: time_start, in: query, schema: { type: string, format: date-time } } + - { name: time_end, in: query, schema: { type: string, format: date-time } } + - { name: status, in: query, description: CSV статусов, schema: { type: string } } + - { name: responsible, in: query, schema: { type: string } } + - { name: executors, in: query, schema: { type: string } } + - { name: with_hierarchy, in: query, schema: { type: boolean, default: true } } + responses: + '200': + description: Страница задач + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: { results: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } + post: + tags: [tasks] + summary: Создать задачу + operationId: createTask + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/pm/msp/tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Задача по id + operationId: retrieveTask + responses: + '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [tasks] + summary: Полное обновление задачи + operationId: updateTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + '400': { $ref: '#/components/responses/BadRequest' } + patch: + tags: [tasks] + summary: Частичное обновление задачи + operationId: partialUpdateTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } + delete: + tags: [tasks] + summary: Удалить задачу + operationId: deleteTask + responses: + '204': { description: Удалено } + + /api/pm/msp/tasks/task_index/: + patch: + tags: [tasks] + summary: Переиндексация задач + operationId: updateTaskIndex + responses: + '200': { description: OK } + + /api/pm/msp/tasks/copy/: + post: + tags: [tasks] + summary: Копировать задачи + operationId: copyTasks + requestBody: + required: true + content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } + responses: + '200': { description: OK } + + /api/pm/msp/tasks/{id}/permissions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Права задачи + operationId: getTaskPermissions + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + post: + tags: [tasks] + summary: Сохранить права задачи + operationId: saveTaskPermissions + requestBody: { required: true, content: { application/json: { schema: { type: object } } } } + responses: + '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } } + + /api/pm/msp/tasks/{id}/descriptions/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Описание задачи + operationId: getTaskDescription + responses: + '200': { description: Описание, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + patch: + tags: [tasks] + summary: Обновить описание задачи + operationId: updateTaskDescription + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } } + + # ========================================================================== + # Detailed tasks / descriptions + # ========================================================================== + /api/pm/msp/detailed-tasks/: + get: + tags: [detailing] + summary: Список детализированных задач + operationId: listDetailedTasks + parameters: + - { name: task, in: query, schema: { type: integer } } + - { name: project, in: query, schema: { type: integer } } + responses: + '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/DetailedTask' } } } } } + post: + tags: [detailing] + summary: Создать детализированную задачу + operationId: createDetailedTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + + /api/pm/msp/detailed-tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [detailing] + summary: Обновить детализированную задачу + operationId: updateDetailedTask + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + responses: + '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } } + delete: + tags: [detailing] + summary: Удалить детализированную задачу + operationId: deleteDetailedTask + responses: + '204': { description: Удалено } + + /api/pm/msp/descriptions/upload_file/: + post: + tags: [tasks] + summary: Загрузить файл описания + operationId: uploadDescriptionFile + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: { type: string, format: binary } + responses: + '200': { description: OK } + + # ========================================================================== + # Values (actual values) + # ========================================================================== + /api/pm/msp/values/: + get: + tags: [tasks] + summary: Фактические значения задач + operationId: listActualValues + parameters: + - { name: task, in: query, schema: { type: integer } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': { description: Значения, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } } + post: + tags: [tasks] + summary: Создать фактическое значение + operationId: createActualValue + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } } + responses: + '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } + + /api/pm/msp/values/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [tasks] + summary: Значение по id + operationId: retrieveActualValue + responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } }, '404': { $ref: '#/components/responses/NotFound' } } + put: + tags: [tasks] + summary: Обновить значение + operationId: updateActualValue + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } } + responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } } + delete: + tags: [tasks] + summary: Удалить значение + operationId: deleteActualValue + responses: { '204': { description: Удалено } } + + # ========================================================================== + # Project states (base plans) + # ========================================================================== + /api/pm/msp/project-states/: + get: + tags: [states] + summary: Список состояний + operationId: listStates + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } } + post: + tags: [states] + summary: Создать состояние (базовый план) + operationId: createState + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } } + responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } + + /api/pm/msp/project-states/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [states] + summary: Состояние по id + operationId: retrieveState + responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } }, '404': { $ref: '#/components/responses/NotFound' } } + put: + tags: [states] + summary: Обновить состояние + operationId: updateState + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } } + responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } + delete: + tags: [states] + summary: Удалить состояние + operationId: deleteState + responses: { '204': { description: Удалено } } + + /api/pm/msp/project-states/{id}/data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: + tags: [states] + summary: Данные состояния (базового плана) + operationId: getStateData + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + post: + tags: [states] + summary: Добавить задачи в базовый план + operationId: addTasksToState + responses: { '200': { description: OK } } + + /api/pm/msp/project-states/{id}/edit_state/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: + tags: [states] + summary: Редактировать состояние + operationId: editState + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: { type: integer } + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + progress: { type: number, nullable: true } + planned_value: { type: number, nullable: true } + unit: { type: string, nullable: true } + required: [id] + responses: { '200': { description: OK } } + + # ========================================================================== + # Resources + # ========================================================================== + /api/pm/msp/resources/: + get: + tags: [resources] + summary: Список ресурсов + operationId: listResources + parameters: + - { name: ids, in: query, schema: { type: string } } + - { name: projects, in: query, schema: { type: string } } + - { name: type, in: query, schema: { type: string } } + - { name: parent, in: query, schema: { type: integer } } + - { name: company, in: query, schema: { type: integer } } + - { name: full, in: query, schema: { type: boolean } } + - { name: extend, in: query, schema: { type: boolean } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Resource' } } } } } } + post: + tags: [resources] + summary: Создать ресурс + operationId: createResource + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } } + responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } + + /api/pm/msp/resources/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [resources], summary: Ресурс по id, operationId: retrieveResource, responses: { '200': { description: Ресурс, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } }, '404': { $ref: '#/components/responses/NotFound' } } } + put: { tags: [resources], summary: Обновить ресурс, operationId: updateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } } + patch: { tags: [resources], summary: Частичное обновление ресурса, operationId: partialUpdateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } } + delete: { tags: [resources], summary: Удалить ресурс, operationId: deleteResource, responses: { '204': { description: Удалено } } } + + /api/pm/msp/resources/bulk_create/: + post: + tags: [resources] + summary: Массовое создание ресурсов + operationId: bulkCreateResources + requestBody: { required: true, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceCreate' } } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/resources/resource_planning/: + get: + tags: [resources] + summary: Ресурсное планирование + operationId: resourcePlanning + parameters: + - { name: projects, in: query, schema: { type: string } } + - { name: resource_type, in: query, schema: { type: string, default: human } } + - { name: start, in: query, required: true, schema: { type: string, format: date-time } } + - { name: end, in: query, required: true, schema: { type: string, format: date-time } } + - { name: scale, in: query, schema: { type: string, default: D } } + - { name: group, in: query, schema: { type: string } } + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + /api/pm/msp/resources/{id}/bind_task/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: { tags: [resources], summary: Привязать задачу к ресурсу, operationId: bindTaskToResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/resources/{id}/bind_elements/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: { tags: [resources], summary: Привязать элементы к ресурсу, operationId: bindElementsToResource, responses: { '201': { description: Создано } } } + + # ---- Resource relations ---- + /api/pm/msp/resources-tasks/: + get: + tags: [resources] + summary: Список привязок ресурс-задача + operationId: listResourceTaskRelations + parameters: + - { name: projects, in: query, schema: { type: string } } + - { name: tasks, in: query, schema: { type: string } } + - { name: resources, in: query, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } } + post: { tags: [resources], summary: Создать привязку ресурс-задача, operationId: createResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/resources-tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [resources], summary: Привязка по id, operationId: retrieveResourceTaskRelation, responses: { '200': { description: Привязка, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } } + patch: { tags: [resources], summary: Обновить привязку, operationId: updateResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [resources], summary: Удалить привязку, operationId: deleteResourceTaskRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/resources-tasks/assign/: + patch: { tags: [resources], summary: Назначение ресурса на задачи, operationId: assignResourceToTasks, responses: { '200': { description: OK } } } + + /api/pm/msp/resources-tasks/bulk_update/: + patch: + tags: [resources] + summary: Массовое обновление привязок ресурс-задача + operationId: bulkUpdateResourceTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } }, profile: { type: integer } } } } } } + responses: { '200': { description: OK } } + + /api/pm/msp/resources-tasks/bulk_delete/: + delete: + tags: [resources] + summary: Массовое удаление привязок ресурс-задача + operationId: bulkDeleteResourceTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } } + responses: { '204': { description: Удалено } } + + /api/pm/msp/resources-elements/: + get: + tags: [resources] + summary: Список привязок ресурс-элемент + operationId: listResourceElementRelations + parameters: + - { name: resources, in: query, schema: { type: string } } + - { name: instances, in: query, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceElementRelation' } } } } } } + post: { tags: [resources], summary: Создать привязку ресурс-элемент, operationId: createResourceElementRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceElementRelation' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Relations (task/project) + # ========================================================================== + /api/pm/msp/task-relations/: + get: + tags: [relations] + summary: Список связей задач + operationId: listTaskRelations + parameters: [ { name: project, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Task2TaskRelation' } } } } } } + post: { tags: [relations], summary: Создать связь задач, operationId: createTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/task-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + put: { tags: [relations], summary: Обновить связь задач, operationId: updateTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить связь задач, operationId: deleteTaskRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/task-relations/bulk_delete/: + delete: + tags: [relations] + summary: Массовое удаление связей задач + operationId: bulkDeleteTaskRelations + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } } + responses: { '204': { description: Удалено } } + + /api/pm/msp/project-relations/: + get: { tags: [relations], summary: Список связей проектов, operationId: listProjectRelations, parameters: [ { name: project, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } } } } } } + post: + tags: [relations] + summary: Создать связь(и) проектов + operationId: createProjectRelation + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/Project2ProjectRelation' }, { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } ] } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/project-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [relations], summary: Обновить связь проектов, operationId: updateProjectRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Project2ProjectRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить связь проектов, operationId: deleteProjectRelation, responses: { '204': { description: Удалено } } } + + /api/pm/msp/entity-relations/: + get: { tags: [relations], summary: Список сущностных связей, operationId: listEntityRelations, parameters: [ { name: project_id, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/EntityRelation' } } } } } } } + post: { tags: [relations], summary: Создать сущностную связь, operationId: createEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/entity-relations/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [relations], summary: Обновить сущностную связь, operationId: updateEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [relations], summary: Удалить сущностную связь, operationId: deleteEntityRelation, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Attributes + # ========================================================================== + /api/pm/msp/project-attribute/: + get: + tags: [attributes] + summary: Атрибуты проекта + operationId: listProjectAttributes + parameters: [ { name: project, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } } } } + post: + tags: [attributes] + summary: Привязать атрибут(ы) к проекту + operationId: createProjectAttribute + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/ProjectAttributeRelation' }, { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } ] } } } } + responses: { '201': { description: Создано } } + + /api/pm/msp/project-attribute/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + put: { tags: [attributes], summary: Обновить атрибут проекта, operationId: updateProjectAttribute, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [attributes], summary: Удалить атрибут проекта, operationId: deleteProjectAttribute, responses: { '204': { description: Удалено } } } + + /api/pm/msp/task-value/: + get: + tags: [attributes] + summary: Значения атрибутов задач + operationId: listTaskValues + parameters: + - { name: tasks, in: query, description: CSV, schema: { type: string } } + - { name: attributes, in: query, description: CSV, schema: { type: string } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } } } } } + post: + tags: [attributes] + summary: Создать значение(я) атрибутов задач + operationId: createTaskValue + requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/TaskAttributeValue' }, { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } ] } } } } + responses: { '201': { description: Создано } } + + # ========================================================================== + # Calendars + # ========================================================================== + /api/pm/msp/calendars/: + get: { tags: [calendars], summary: Список календарей, operationId: listCalendars, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Calendar' } } } } } } } + post: { tags: [calendars], summary: Создать календарь, operationId: createCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } } + + /api/pm/msp/calendars/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [calendars], summary: Календарь по id, operationId: retrieveCalendar, responses: { '200': { description: Календарь, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } } + patch: { tags: [calendars], summary: Изменить календарь, operationId: updateCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [calendars], summary: Удалить календарь, operationId: deleteCalendar, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Project settings (KSG) + # ========================================================================== + /api/pm/msp/project-settings/: + get: { tags: [settings], summary: Список настроек КСГ, operationId: listProjectSettings, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectSettings' } } } } } } } + post: { tags: [settings], summary: Создать настройку КСГ, operationId: createProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/project-settings/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [settings], summary: Настройка по id, operationId: retrieveProjectSettings, responses: { '200': { description: Настройка, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } } } } + patch: { tags: [settings], summary: Изменить настройку, operationId: updateProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [settings], summary: Удалить настройку, operationId: deleteProjectSettings, responses: { '204': { description: Удалено } } } + + /api/pm/msp/project-settings/{id}/apply/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + post: + tags: [settings] + summary: Применить глобальную настройку + operationId: applyProjectSettings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_id: { type: integer } + connect_missing_attributes: { type: boolean } + required: [project_id] + responses: { '200': { description: OK } } + + # ========================================================================== + # Visual profiles + # ========================================================================== + /api/pm/msp/visual-profiles/: + get: { tags: [detailing], summary: Список визуальных профилей, operationId: listVisualProfiles, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } } } } + post: { tags: [detailing], summary: Создать визуальный профиль, operationId: createVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '201': { description: Создано } } } + + /api/pm/msp/visual-profiles/{id}: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + patch: { tags: [detailing], summary: Изменить визуальный профиль, operationId: updateVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '200': { description: Обновлено } } } + delete: { tags: [detailing], summary: Удалить визуальный профиль, operationId: deleteVisualProfile, responses: { '204': { description: Удалено } } } + + # ========================================================================== + # Comments + # ========================================================================== + /api/pm/msp/comments/: + get: { tags: [comments], summary: Список комментариев, operationId: listComments, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Comment' } } } } } } } + post: { tags: [comments], summary: Создать комментарий, operationId: createComment, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CommentCreate' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Descriptions resource + # ========================================================================== + /api/pm/msp/descriptions/: + get: { tags: [tasks], summary: Список описаний, operationId: listDescriptions, parameters: [ { name: task, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Description' } } } } } } } + post: { tags: [tasks], summary: Создать описание, operationId: createDescription, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }, responses: { '201': { description: Создано } } } + + # ========================================================================== + # Task info (public/service) + # ========================================================================== + /api/pm/msp/task-info/: + get: + tags: [tasks] + summary: Задачи по профилям (not_started/in_progress/completed) + operationId: getTaskInfo + parameters: + - { name: projects, in: query, required: true, description: CSV идентификаторов, schema: { type: string } } + - { name: date, in: query, required: true, schema: { type: string, format: date-time } } + - { name: document, in: query, required: true, schema: { type: string } } + - { name: base_plane, in: query, schema: { type: string } } + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + /api/pm/msp/external-task-info/: + get: + tags: [tasks] + summary: Инфо о фоновой задаче через брокер + operationId: getExternalTaskInfo + security: [] + parameters: [ { name: task_id, in: query, schema: { type: string, format: uuid } } ] + responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } + + # ========================================================================== + # External API v2 (read-only) + # ========================================================================== + /api/pm/external/v2/projects/: + get: + tags: [external] + summary: Список проектов (внешний) + operationId: externalListProjects + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - { name: search, in: query, schema: { type: string } } + - { name: ids, in: query, schema: { type: string } } + - { name: parent_id, in: query, schema: { type: integer } } + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project' } } } } } } + + /api/pm/external/v2/projects/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Проект (внешний), operationId: externalRetrieveProject, responses: { '200': { description: Проект, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }, '404': { $ref: '#/components/responses/NotFound' } } } + + /api/pm/external/v2/projects/{id}/project_data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Данные проекта (внешний), operationId: externalProjectData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } } + + /api/pm/external/v2/tasks/: + get: + tags: [external] + summary: Список задач (внешний) + operationId: externalListTasks + parameters: + - { name: extend, in: query, schema: { type: boolean } } + - { name: ids, in: query, schema: { type: string } } + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } } + + /api/pm/external/v2/tasks/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Задача (внешний), operationId: externalRetrieveTask, responses: { '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } } } + + /api/pm/external/v2/values/: + get: { tags: [external], summary: Фактические значения (внешний), operationId: externalListValues, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } } } } + + /api/pm/external/v2/values/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Значение (внешний), operationId: externalRetrieveValue, responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } } } + + /api/pm/external/v2/project-states/: + get: + tags: [external] + summary: Состояния проекта (внешний) + operationId: externalListStates + parameters: [ { name: project_id, in: query, schema: { type: integer } } ] + responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } } + + /api/pm/external/v2/project-states/{id}/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Состояние (внешний), operationId: externalRetrieveState, responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } } + + /api/pm/external/v2/project-states/{id}/data/: + parameters: [ { $ref: '#/components/parameters/PathId' } ] + get: { tags: [external], summary: Данные состояния (внешний), operationId: externalStateData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } } + + # ========================================================================== + # Internal API (no auth, cluster-only) + # ========================================================================== + /internal/pm/auto_scheduling/: + post: + tags: [internal] + summary: Интегрированное автопланирование + operationId: internalAutoScheduling + security: [] + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } } + responses: { '200': { description: OK } } + + /internal/pm/integrated_base_plan/: + post: + tags: [internal] + summary: Создать/обновить интегрированные состояния + operationId: internalIntegratedBasePlan + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + project_id: { type: integer } + user_id: { type: integer } + linked_project: { type: integer } + name: { type: string } + tasks: { type: array, items: { type: object } } + task_ids: { type: array, items: { type: integer } } + state_id: { type: integer } + responses: { '200': { description: OK } } + + /internal/pm/detailed_tasks/: + post: + tags: [internal] + summary: Обновить значения атрибутов задачи + operationId: internalDetailedTasks + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + task_id: { type: integer } + result_fields: { type: object } + company_id: { type: integer } + required: [task_id, company_id] + responses: { '200': { description: OK } } + + /internal/pm/sync_tasks/: + post: + tags: [internal] + summary: Синхронизация задач + автопланирование + operationId: internalSyncTasks + security: [] + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } } + responses: { '200': { description: OK } } + + /internal/pm/base_plans/: + get: + tags: [internal] + summary: Получить базовый план + operationId: internalGetBasePlan + security: [] + parameters: [ { name: base_plan_id, in: query, required: true, schema: { type: integer } } ] + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + '400': { $ref: '#/components/responses/BadRequest' } + + /internal/pm/tasks_dates/: + get: + tags: [internal] + summary: Даты и прогресс задач проекта + operationId: internalTasksDates + security: [] + parameters: [ { name: project_id, in: query, required: true, schema: { type: integer } } ] + responses: + '200': { description: Данные, content: { application/json: { schema: { type: object } } } } + '400': { $ref: '#/components/responses/BadRequest' } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT в заголовке `Authorization: Bearer `. В режиме sarex-backend + подпись проверяется публичным ключом (`RS512`); в режиме Zitadel + дополнительно требуется заголовок `identity`. + identityToken: + type: apiKey + in: header + name: identity + description: | + Заголовок `identity` (`Identity `) для режима Zitadel. При его + наличии полезная нагрузка берётся из этого токена. + + parameters: + PathId: + name: id + in: path + required: true + schema: { type: integer } + Limit: + name: limit + in: query + required: false + schema: { type: integer, minimum: 0, default: 1000 } + Offset: + name: offset + in: query + required: false + schema: { type: integer, minimum: 0, default: 0 } + + responses: + BadRequest: + description: Некорректный запрос / ошибка валидации + content: + application/json: + schema: { $ref: '#/components/schemas/ValidationError' } + Unauthorized: + description: Токен не предоставлен или невалиден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + Forbidden: + description: Недостаточно прав + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + NotFound: + description: Ресурс не найден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + + schemas: + # --- Общие ------------------------------------------------------------ + Detail: + type: object + properties: + detail: { type: string } + required: [detail] + + ValidationError: + type: object + description: | + Ошибка валидации DRF — либо словарь `{ "": ["", ...] }`, + либо `{ "detail": "..." }`. + additionalProperties: + type: array + items: { type: string } + + PaginatedResponse: + type: object + description: Обёртка DRF LimitOffsetPagination + properties: + count: { type: integer } + next: { type: string, format: uri, nullable: true } + previous: { type: string, format: uri, nullable: true } + results: { type: array, items: { type: object } } + required: [count, results] + + # --- Проекты ---------------------------------------------------------- + Project: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + parent: { type: integer, nullable: true } + is_folder: { type: boolean } + resource_id: { type: string, nullable: true } + document_id: { type: string, nullable: true } + bundle_id: { type: string, nullable: true } + description: { type: string, nullable: true } + settings: { type: object } + target_date: { type: string, format: date-time, nullable: true } + project_type: { type: string, nullable: true } + calendar: { type: integer, nullable: true } + status: { type: string, readOnly: true } + creator: { type: integer, readOnly: true } + permissions: { type: object, readOnly: true } + restrictions: { type: object, readOnly: true } + time_start: { type: string, format: date-time, readOnly: true, nullable: true } + time_end: { type: string, format: date-time, readOnly: true, nullable: true } + total_cost: { type: number, readOnly: true, nullable: true } + current_cost: { type: number, readOnly: true, nullable: true } + progress: { type: number, readOnly: true, nullable: true } + relations: { type: array, items: { type: object }, readOnly: true } + rule: { type: object, nullable: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, company] + + ProjectCreate: + type: object + properties: + name: { type: string } + company_id: { type: integer } + parent: { type: integer, nullable: true } + is_folder: { type: boolean } + resource_id: { type: string, nullable: true } + ms_file: { type: string, nullable: true } + description: { type: string, nullable: true } + document_id: { type: string, nullable: true } + bundle_id: { type: string, nullable: true } + settings: { type: object, default: {} } + target_date: { type: string, format: date-time, nullable: true } + project_type: { type: string, nullable: true } + calendar: { type: integer, nullable: true } + required: [name, company_id] + + FolderCreate: + type: object + properties: + company: { type: integer } + company_name: { type: string } + creator: { type: integer } + service_account: { type: string, format: uuid } + folder_name: { type: string, nullable: true } + required: [company, company_name, creator, service_account] + + PermissionsResponse: + type: object + properties: + direct_permissions: { type: array, items: { type: object } } + inherited_permissions: { type: array, items: { type: object } } + + # --- Задачи ----------------------------------------------------------- + ProjectTask: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + parent: { type: integer, nullable: true } + index_prefix: { type: string, nullable: true } + task_id: { type: string, nullable: true } + name: { type: string } + time_start: { type: string, format: date-time, nullable: true } + time_end: { type: string, format: date-time, nullable: true } + deadline: { type: string, format: date-time, nullable: true } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + task_type: { type: string } + unit: { type: string, nullable: true } + local_index: { type: integer, nullable: true } + constraint_date: { type: string, format: date-time, nullable: true } + constraint_type: { type: string, nullable: true } + cost: { type: number, nullable: true } + progress: { type: number, nullable: true } + status: { type: string, nullable: true } + duration: { type: number, nullable: true } + planned_duration: { type: number, nullable: true } + cipher: { type: string, nullable: true } + is_active: { type: boolean } + current_cost: { type: number, nullable: true } + planned_value: { type: number, nullable: true } + calendar: { type: integer, nullable: true } + distribution: { type: object, nullable: true } + actual_value: { type: number, readOnly: true, nullable: true } + total_cost: { type: number, readOnly: true, nullable: true } + attributes: { type: array, items: { type: object } } + permissions: { type: object } + + ProjectTaskCreate: + type: object + properties: + project: { type: integer } + parent: { type: integer, nullable: true } + name: { type: string } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + deadline: { type: string, format: date-time, nullable: true } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + task_type: { type: string, default: fix_value } + unit: { type: string, nullable: true } + cost: { type: number, nullable: true } + progress: { type: number, nullable: true } + status: { type: string, nullable: true } + is_active: { type: boolean, default: true } + calendar: { type: integer, nullable: true } + required: [project, name, time_start, time_end] + + ProjectTaskUpdate: + type: object + description: Плоский сериализатор для массового обновления + properties: + id: { type: integer } + name: { type: string } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + responsible: { type: array, items: { type: integer } } + executors: { type: array, items: { type: integer } } + cost: { type: number } + progress: { type: number } + status: { type: string } + required: [id] + + ActualValue: + type: object + properties: + id: { type: integer, readOnly: true } + value: { type: number } + description: { type: string, nullable: true } + task: { type: integer } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + ActualValueCreate: + type: object + properties: + value: { type: number } + description: { type: string, nullable: true } + task: { type: integer } + time_start: { type: string, format: date-time } + time_end: { type: string, format: date-time } + required: [value, task, time_start, time_end] + + DetailedTask: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + weight: { type: number, nullable: true } + data: { type: object } + + Description: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + text: { type: string } + required: [task, text] + + # --- Состояния -------------------------------------------------------- + ProjectState: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + project: { type: integer } + creator: { type: integer, readOnly: true } + is_locked: { type: boolean, readOnly: true } + + ProjectStateCreate: + type: object + properties: + name: { type: string } + project: { type: integer } + required: [name, project] + + # --- Ресурсы ---------------------------------------------------------- + Resource: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + calendar: { type: integer, nullable: true } + project: { type: integer, nullable: true } + resource_type: { type: string } + parent: { type: integer, nullable: true } + cost: { type: number, nullable: true } + path: { type: string, readOnly: true } + is_leaf: { type: boolean, readOnly: true } + elements: { type: array, items: { type: integer } } + projects: { type: array, items: { type: object } } + + ResourceCreate: + type: object + properties: + name: { type: string } + company: { type: integer } + calendar: { type: integer, nullable: true } + project: { type: integer, nullable: true } + resource_type: { type: string } + parent: { type: integer, nullable: true } + cost: { type: number, nullable: true } + required: [name, company] + + ResourceTaskRelation: + type: object + properties: + id: { type: integer, readOnly: true } + resource: { type: integer } + task: { type: integer } + profile: { type: integer, nullable: true } + + ResourceElementRelation: + type: object + properties: + id: { type: integer, readOnly: true } + resource: { type: integer } + instance: { type: string } + model_name: { type: string } + attributes: { type: object } + path: { type: string, nullable: true } + + # --- Связи ------------------------------------------------------------ + Task2TaskRelation: + type: object + properties: + id: { type: integer, readOnly: true } + target: { type: integer } + source: { type: integer } + type: { type: string } + lag: { type: number, nullable: true } + + Project2ProjectRelation: + type: object + properties: + id: { type: integer, readOnly: true } + successor: { type: integer } + predecessor: { type: integer } + mode: { type: string, nullable: true } + + EntityRelation: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + entity_name: { type: string } + attribute_ids: { type: array, items: { type: integer } } + settings: { type: object } + entity_type: { type: string } + + # --- Атрибуты --------------------------------------------------------- + ProjectAttributeRelation: + type: object + properties: + id: { type: integer, readOnly: true } + project: { type: integer } + asset_id: { type: string, nullable: true } + attribute_id: { type: integer } + is_system: { type: boolean } + default_value: { nullable: true } + mode: { type: string, nullable: true } + + TaskAttributeValue: + type: object + properties: + id: { type: integer, readOnly: true } + task: { type: integer } + attribute_id: { type: integer } + type: { type: string, description: "Тип значения (вход): integer/float/string/datetime/date" } + values: { description: Значение(я) атрибута } + integer_value: { type: integer, nullable: true } + float_value: { type: number, nullable: true } + string_value: { type: string, nullable: true } + datetime_value: { type: string, format: date-time, nullable: true } + date_value: { type: string, format: date, nullable: true } + assets: { type: array, items: { type: string } } + + # --- Календари -------------------------------------------------------- + Calendar: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + company: { type: integer } + mask: { type: array, items: { type: integer } } + work_time_start: { type: string } + work_time_end: { type: string } + work_day_duration: { type: number, readOnly: true } + is_system: { type: boolean } + holidays: { type: array, items: { type: string, format: date } } + exception_holidays: { type: array, items: { type: string, format: date } } + creator: { type: integer, readOnly: true } + projects_count: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [name, mask, work_time_start, work_time_end] + + # --- Настройки -------------------------------------------------------- + ProjectSettings: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + project: { type: integer, nullable: true } + state: { type: integer, nullable: true } + image: { type: string, nullable: true } + is_default: { type: boolean } + visibility: { type: string, enum: [global, private, public] } + company_id: { type: integer, nullable: true } + service_accounts: { type: array, items: { type: string } } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + VisualProfile: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string } + color: { type: string, description: "HEX-цвет, напр. #RRGGBB" } + project: { type: integer } + behavior: { type: object } + + Comment: + type: object + properties: + id: { type: integer, readOnly: true } + text: { type: string } + task: { type: integer } + creator: { type: integer, readOnly: true } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + + CommentCreate: + type: object + properties: + text: { type: string } + task: { type: integer } + required: [text, task] + + # --- Системный журнал ------------------------------------------------- + SystemLog: + type: object + properties: + id: { type: integer, readOnly: true } + project_id: { type: integer } + user_id: { type: integer, nullable: true } + created_at: { type: string, format: date-time, readOnly: true } + message_data: { type: object } + action: { type: string } + entity: { type: string } + changes: { type: object, description: "Только в detail-варианте" } + + # --- Экспорт ---------------------------------------------------------- + ExportProject: + type: object + properties: + export_fields: { type: array, items: { type: string } } + export_attrs: { type: array, items: { type: integer } } + fields_order: { type: array, items: { type: string } } + data: { type: array, items: { type: object } } + page_format: { type: string } + page_size: { type: string } + left_table_percent_width: { type: number, minimum: 0, maximum: 100 } + base_plan_ids: { type: array, items: { type: integer } } + margins: { type: array, items: { type: number } } + expanded_tasks: { type: array, items: { type: integer } } + header_text: { type: string } + footer_text: { type: string } + display_relations: { type: boolean } + date_from: { type: string, format: date } + date_to: { type: string, format: date } + fields_width: { type: object } + project_name: { type: string } + show_counter: { type: boolean } + font_size: { type: integer } + conditional_formatting: { type: array, items: { type: object } } + required: [export_fields, data] diff --git a/apps/prescriptions/.env.example b/apps/prescriptions/.env.example new file mode 100644 index 0000000..8b53450 --- /dev/null +++ b/apps/prescriptions/.env.example @@ -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= diff --git a/apps/prescriptions/CONFIGURATION.md b/apps/prescriptions/CONFIGURATION.md new file mode 100644 index 0000000..9aead94 --- /dev/null +++ b/apps/prescriptions/CONFIGURATION.md @@ -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` (внедрение `___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`. diff --git a/apps/prescriptions/ENDPOINTS.md b/apps/prescriptions/ENDPOINTS.md new file mode 100644 index 0000000..eb09e39 --- /dev/null +++ b/apps/prescriptions/ENDPOINTS.md @@ -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` в текущем коде модуля не используется). diff --git a/apps/processing/workflows-api.CONFIGURATION.md b/apps/processing/workflows-api.CONFIGURATION.md new file mode 100644 index 0000000..a86d666 --- /dev/null +++ b/apps/processing/workflows-api.CONFIGURATION.md @@ -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 его нужно добавить. diff --git a/apps/processing/workflows-api.env.example b/apps/processing/workflows-api.env.example new file mode 100644 index 0000000..434d7ff --- /dev/null +++ b/apps/processing/workflows-api.env.example @@ -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 +# ============================================================================ diff --git a/apps/processing/workflows-api.openapi.yaml b/apps/processing/workflows-api.openapi.yaml new file mode 100644 index 0000000..6393cbf --- /dev/null +++ b/apps/processing/workflows-api.openapi.yaml @@ -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 `** — токен Sarex, подпись проверяется публичным ключом + из переменной `PUBLIC_KEY` (RS/PKIX). Из claims извлекаются `user_id`, `company_ids`, + `is_superuser`. + - **`Identity: Bearer `** — токен 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 ` — токен Sarex. Альтернативно можно передать заголовок + `Identity: Bearer ` (токен 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 diff --git a/apps/processing/workflows-engine.CONFIGURATION.md b/apps/processing/workflows-engine.CONFIGURATION.md new file mode 100644 index 0000000..a9556a1 --- /dev/null +++ b/apps/processing/workflows-engine.CONFIGURATION.md @@ -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/`. diff --git a/apps/processing/workflows-engine.env.example b/apps/processing/workflows-engine.env.example new file mode 100644 index 0000000..da8daf2 --- /dev/null +++ b/apps/processing/workflows-engine.env.example @@ -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) +# ============================================================================ diff --git a/apps/processing/workflows-frontend.ENDPOINTS.md b/apps/processing/workflows-frontend.ENDPOINTS.md new file mode 100644 index 0000000..b5af211 --- /dev/null +++ b/apps/processing/workflows-frontend.ENDPOINTS.md @@ -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, либо один из репозиториев устарел. diff --git a/apps/projects/CONFIGURATION.md b/apps/projects/CONFIGURATION.md new file mode 100644 index 0000000..627c17f --- /dev/null +++ b/apps/projects/CONFIGURATION.md @@ -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" ? :
`). + +Отдельного конфиг-файла (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-.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= --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-`. Стадии: `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-`. Значения не синхронизированы. +- **Приватный реестр.** Установка зависимостей требует валидный `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`). diff --git a/apps/projects/ENDPOINTS.md b/apps/projects/ENDPOINTS.md new file mode 100644 index 0000000..b6775e2 --- /dev/null +++ b/apps/projects/ENDPOINTS.md @@ -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`.