From 5f7fe117483dbc3906dd795a174b63969d0ac311 Mon Sep 17 00:00:00 2001 From: emelinda Date: Mon, 13 Jul 2026 20:35:36 +0300 Subject: [PATCH] Add example `.env` files and configuration documentation for `measurements` and `subscriptions` services. --- apps/measurements/.env.example | 50 ++ apps/measurements/CONFIGURATION.md | 150 +++++ apps/measurements/openapi.json | 927 ++++++++++++++++++++++++++++ apps/measurements/openapi.yaml | 565 +++++++++++++++++ apps/subscriptions/.env.example | 64 ++ apps/subscriptions/CONFIGURATION.md | 200 ++++++ apps/subscriptions/openapi.yaml | 719 +++++++++++++++++++++ apps/workspaces/.env.example | 66 +- apps/workspaces/CONFIGURATION.md | 166 +++-- 9 files changed, 2823 insertions(+), 84 deletions(-) create mode 100644 apps/measurements/.env.example create mode 100644 apps/measurements/CONFIGURATION.md create mode 100644 apps/measurements/openapi.json create mode 100644 apps/measurements/openapi.yaml create mode 100644 apps/subscriptions/.env.example create mode 100644 apps/subscriptions/CONFIGURATION.md create mode 100644 apps/subscriptions/openapi.yaml diff --git a/apps/measurements/.env.example b/apps/measurements/.env.example new file mode 100644 index 0000000..16be797 --- /dev/null +++ b/apps/measurements/.env.example @@ -0,0 +1,50 @@ +# ============================================================================ +# measurements — пример переменных окружения (HTTP-сервис на FastAPI) +# +# Скопируйте нужные строки в config.env / .env сервиса. +# Переменные, помеченные (обяз.), обязательны — без них процесс не стартует. +# Конфигурация читается через pydantic-settings (src/measurements/config.py). +# bool принимает 1/0, true/false, yes/no. +# ============================================================================ + +# --- S3 / MinIO (обяз.) ----------------------------------------------------- +# Единственная обязательная переменная. JSON-строка с доступами к S3. +# Разбирается в S3CredentialsSettings.from_env(); если не задана — +# ValueError и процесс не стартует. +# Поля: host, login, password (обяз.), verify (bool, по умолч. false), +# buckets (список; если пуст — читается через list_buckets()). +S3_JSON_SETTINGS='{"host":"https://s3.example.com","login":"login","password":"password","verify":false,"buckets":["measurements"]}' + +# --- Логирование (префикс LOG_) --------------------------------------------- +LOG_LEVEL=INFO # уровень логирования (по умолчанию INFO) +# LOG_FORMAT='{"timestamp": "%(asctime)s", "level": "%(levelname)s", "message": "%(message)s"}' # формат JSON-лога + +# --- Приложение (ApplicationSettings, без префикса) ------------------------- +# AUTH=0 # включить CustomAuthenticationMiddleware (по умолч. false) +# SHOW_UI=0 # показывать Swagger/redoc (по умолч. false — docs отключены) +# USE_SENTRY=0 # инициализировать Sentry (по умолч. false) +# DEBUG=0 # флаг отладки (по умолч. false) +# CLASSIC_MODE=1 # классический режим расчётов (по умолч. true) +# BLOCK_SIZE=256 # размер блока обработки растра (по умолч. 256) +# BLOCK_SIZE_FACTOR=10 # множитель площади блока (по умолч. 10) +# CPU_NUMBER=10 # число используемых CPU (по умолч. 10) + +# --- Django / ЛК (префикс DJANGO_; читается при AUTH=1) ---------------------- +# DJANGO_USE=1 # использовать интеграцию с Django (по умолч. true) +DJANGO_HOST=https://lk.sarex.io # базовый URL Django/ЛК (по умолч. https://lk.sarex.io) +# DJANGO_TIMEOUT=10 # таймаут HTTP-запросов к Django, сек (по умолч. 10) + +# --- Sentry (префикс SENTRY_; читается при USE_SENTRY=1) --------------------- +# SENTRY_DSN= # DSN проекта Sentry (по умолч. пусто) +# SENTRY_ENVIRONMENT=production # окружение (по умолч. production) +# SENTRY_TRACES_SAMPLE_RATE=1.0 # доля трейсов (по умолч. 1.0) +# SENTRY_SEND_DEFAULT_PII=1 # отправлять PII (по умолч. true) + +# --- Трейсинг OpenTelemetry (префикс TRACING_; читается при TRACING_USE=1) --- +TRACING_USE=0 # включить OTEL-трейсинг и otel-логгер (по умолч. false) +# TRACING_HOST=localhost:4317 # адрес OTLP-коллектора (по умолч. localhost:4317) +# TRACING_SERVICE_NAME=measurements # имя сервиса в трейсах (по умолч. measurements) +# TRACING_INSECURE=0 # подключение без TLS (по умолч. false) + +# --- Задаётся в манифестах, кодом приложения НЕ читается --------------------- +# S3_JSON_FILE=/opt/cred_s3.json # присутствует в .helm/values.yaml, но код читает только S3_JSON_SETTINGS diff --git a/apps/measurements/CONFIGURATION.md b/apps/measurements/CONFIGURATION.md new file mode 100644 index 0000000..d0f993b --- /dev/null +++ b/apps/measurements/CONFIGURATION.md @@ -0,0 +1,150 @@ +# Конфигурация measurements + +Документ описывает все переменные окружения и способы конфигурирования сервиса репозитория `measurements`: + +- **measurements** — HTTP-сервис на FastAPI (`src/measurements`), запускается через gunicorn/uvicorn (`entrypoint.sh`, `measurements.main:app`). Считает измерения по растрам (GeoTIFF), читая их напрямую из S3/MinIO через GDAL (`vsis3`). Отдельного воркера у сервиса нет. + +## Способы конфигурирования + +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/measurements/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): классы `LoggerSettings`, `SentrySettings`, `DjangoSettings`, `ApplicationSettings`, `TraceSettings`, `S3CredentialsSettings`, `Store`. Отдельного конфиг-файла (yaml/toml) у приложения нет. + +Каждый класс задаёт свой префикс через `class Config: env_prefix` (`LOG_`, `SENTRY_`, `DJANGO_`, `TRACING_`, `S3_`); у `ApplicationSettings` префикса нет — её поля читаются по имени напрямую (`AUTH`, `SHOW_UI`, `USE_SENTRY` и т.п.). Почти все переменные имеют значения по умолчанию, поэтому обязательна фактически одна — **`S3_JSON_SETTINGS`**: её отсутствие приводит к `ValueError` в `S3CredentialsSettings.from_env()` и процесс не стартует. + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | `docker-compose.yaml` — образ `measurements`, проброс порта `8000:8000`, инлайн `environment: S3_JSON_SETTINGS`. Сервис запускается `entrypoint.sh` (gunicorn, 4 воркера, uvicorn worker, таймаут 240) | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml` (universal-chart, dependency `oci://…/charts`): блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов). Per-env значения через ключи `_default`/`stage`/`preprod`/`production` | +| Kubernetes — этот infra-репозиторий (`iac/apps/measurements`) | `base/` — kustomize-манифесты с инъекцией секрета S3 через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + патч реплик), `brusnika-stage`/`brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блок `secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); в `workflow.rules` задаются переменные пайплайна (`STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `SERVICE_NAME`, `DOCKERFILE_PATH`) | + +**Миграции БД.** Отсутствуют. Сервис не хранит собственное состояние в реляционной БД (`psycopg2` присутствует в зависимостях, но код измерений работает с растрами из S3). Шага миграций в `entrypoint.sh` нет. + +--- + +## measurements (`measurements`) + +Переменные читаются набором классов `*Settings` в `config.py`, инстанцируемых на уровне модуля: `settings = ApplicationSettings()`, `store = Store()`, `logger = LoggerSettings().logger`, `tracing_settings = TraceSettings()`. + +### S3 / MinIO (обязательно) + +Класс `S3CredentialsSettings`. Единственный обязательный источник конфигурации — переменная `S3_JSON_SETTINGS` (JSON-строка). Валидатор `from_env` (`model_validator(mode='before')`) читает её из окружения и при отсутствии выбрасывает `ValueError`. Доступы к S3 используются как boto3-клиентом (список бакетов), так и GDAL (`AWS_S3_ENDPOINT`/`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, драйвер `vsis3`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `S3_JSON_SETTINGS` | string (JSON) | да | — | JSON с доступами к S3. Поля: `host`, `login`, `password` (обяз.); `verify` (bool, по умолч. `false`); `buckets` (список; если пуст — бакеты запрашиваются через `list_buckets()`). Пример: `{"host":"https://s3…","login":"…","password":"…","verify":false,"buckets":["measurements"]}` | + +> В `host` поддерживаются схемы `http://`/`https://`: при `http://` GDAL переключается на `AWS_HTTPS=NO`, отключает `GDAL_DISABLE_READDIR_ON_OPEN` и `AWS_VIRTUAL_HOSTING`. + +### Логирование (префикс `LOG_`) + +Класс `LoggerSettings`. Настраивает JSON-логгер (`python-json-logger`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `LOG_LEVEL` | string | нет | `INFO` | Уровень логирования (`INFO`/`DEBUG`/…); неизвестное значение → `INFO` | +| `LOG_FORMAT` | string | нет | JSON-шаблон | Формат строки лога для `JsonFormatter` | + +### Приложение (`ApplicationSettings`, без префикса) + +Поля читаются по имени напрямую (регистронезависимо). Управляют поведением сервиса и подключением middleware в `main.py`. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `AUTH` | bool | нет | `false` | Подключить `CustomAuthenticationMiddleware` (проверка JWT `authorization`/`identity`) | +| `SHOW_UI` | bool | нет | `false` | Включить Swagger/redoc; при `false` `docs_url`/`redoc_url` отключены | +| `USE_SENTRY` | bool | нет | `false` | Инициализировать Sentry SDK и `SentryAsgiMiddleware` | +| `DEBUG` | bool | нет | `false` | Флаг отладки | +| `CLASSIC_MODE` | bool | нет | `true` | Классический режим расчётов | +| `BLOCK_SIZE` | int | нет | `256` | Размер блока обработки растра; участвует в `area_factor` | +| `BLOCK_SIZE_FACTOR` | int | нет | `10` | Множитель площади блока (`area_factor = BLOCK_SIZE_FACTOR × BLOCK_SIZE²`) | +| `CPU_NUMBER` | int | нет | `10` | Число используемых CPU | + +> `DEBUG`, `CLASSIC_MODE`, `BLOCK_SIZE`, `BLOCK_SIZE_FACTOR`, `CPU_NUMBER` задаются в конфиге, но в текущих обработчиках напрямую не считываются (в `main.py` используются только `SHOW_UI`, `USE_SENTRY`, `AUTH`). Оставлены как настраиваемые параметры. + +### Django / ЛК (префикс `DJANGO_`) + +Класс `DjangoSettings`. Используется `CustomAuthenticationMiddleware`/`DjangoUserMiddleware` при включённой авторизации (`AUTH=1`) для запросов к ЛК. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `DJANGO_USE` | bool | нет | `true` | Использовать интеграцию с Django | +| `DJANGO_HOST` | string | нет | `https://lk.sarex.io` | Базовый URL Django/ЛК | +| `DJANGO_TIMEOUT` | int | нет | `10` | Таймаут HTTP-запросов к Django, сек | + +### Sentry (префикс `SENTRY_`) + +Класс `SentrySettings`. Значения передаются в `sentry_sdk.init(**settings.sentry.kwargs)` только при `USE_SENTRY=1`. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SENTRY_DSN` | string | нет | `""` | DSN проекта Sentry | +| `SENTRY_ENVIRONMENT` | string | нет | `production` | Имя окружения в Sentry | +| `SENTRY_TRACES_SAMPLE_RATE` | float | нет | `1.0` | Доля трейсов | +| `SENTRY_SEND_DEFAULT_PII` | bool | нет | `true` | Отправлять PII | + +### Трейсинг (OpenTelemetry, префикс `TRACING_`) + +Класс `TraceSettings`. Активируется при `TRACING_USE=1` (`fastapi-otel-tools`). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `TRACING_USE` | bool | нет | `false` | Включает OTEL-трейсинг, otel-логгер и `OtelMiddleware` | +| `TRACING_HOST` | string | нет | `localhost:4317` | Адрес OTLP-коллектора | +| `TRACING_SERVICE_NAME` | string | нет | `measurements` | Имя сервиса в трейсах | +| `TRACING_INSECURE` | bool | нет | `false` | Небезопасное (без TLS) подключение к коллектору | + +> Тип `bool` в pydantic принимает `1`/`0`, `true`/`false`, `yes`/`no`. + +--- + +## Инфраструктурные и вспомогательные переменные + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `S3_JSON_FILE` | `.helm/values.yaml` (`envs`) | Путь к файлу с доступами S3 (`/opt/cred_s3.json`). **Кодом не читается** — приложение использует только `S3_JSON_SETTINGS` | +| `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` (`variables`) | Параметры сборки образа | +| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `universal-pipeline` (деплой чарта per-env: `stage`/`preprod`/`production`) | + +--- + +## Деплой из этого репозитория (`iac/apps/measurements`) + +В `base/` используется **kustomize** (не собственный Helm-чарт сервиса). Доступ к S3/MinIO инъектируется агентом **Vault** и подгружается в окружение процесса до старта. + +### `base/` + +- `deployment.yaml` — единственный Deployment `measurements` (namespace `measurements`). Аннотации Vault (`agent-inject`, `role: measurements`) формируют шаблон секрета `measurements-s3` из `secrets/data/minio/apps/measurements`, собирая `S3_JSON_SETTINGS='{"host":…,"login":…,"password":…,"verify":false,"buckets":["measurements"]}'`. Контейнер запускается командой `set -a; . /vault/secrets/measurements-s3; set +a; exec /opt/entrypoint.sh`. Инлайн задан только `TRACING_USE=false`. Порт `8000` (`http`), `serviceAccountName: measurements-vault`, `imagePullSecrets: regcred`, ресурсы `cpu 25m` / `memory 128Mi`. +- `service.yaml` — `Service` `measurements-svc` (ClusterIP, порт `8000` → `8000`). +- `namespace.yaml` — namespace `measurements` с `istio-injection: enabled`. +- `serviceaccount.yaml` — SA `measurements-vault`. +- `kustomization.yaml` собирает `namespace`, `serviceaccount`, `deployment`, `service`. + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + патч `replicas.yaml` (реплики Deployment `measurements` = 1). +- **`brusnika-stage`** / **`brusnika-prod`** — Flux `HelmRelease` `measurements` на `universal-chart` `0.1.7` (source `yc-oci-charts`). Секрет `S3_JSON_SETTINGS` берётся из k8s-секрета `s3-json-settings` (`secretEnvs`). `replicaCount`: `stage 1`, `preprod 3`, `production 3`; `imagePullSecrets: regcred`; `labels.monitoring: prometheus`; сервис `measurements-service` (ClusterIP, `8000`). + +--- + +## Замечания и потенциальные проблемы + +- **`S3_JSON_SETTINGS` — единственная жёстко обязательная переменная.** Локальный `docker-compose.yaml` задаёт её значением-заглушкой (`{"host":"host","login":"login","password":"password"}`) — для реальной работы значение нужно заменить. +- **`S3_JSON_FILE` в `.helm/values.yaml` кодом не читается** — приложение использует только `S3_JSON_SETTINGS` (в этом infra-репозитории она и инъектируется Vault). Расхождение способов передачи доступов между собственным чартом и infra-репо. +- **Опечатка в `middleware.py`:** в `DjangoUserMiddleware` используется `settings.django.self.timeout` вместо `settings.django.timeout` — лишний `.self` приведёт к `AttributeError`. Сам `DjangoUserMiddleware` в `main.py` не подключается (подключается `CustomAuthenticationMiddleware`). +- **Отсутствует поле `jwt_public_key`:** `CustomAuthenticationMiddleware` при отсутствии заголовка `identity` вызывает `jwt.decode(key=settings.jwt_public_key, …)`, но такого поля в `ApplicationSettings` нет — при `AUTH=1` и запросе без `identity` это приведёт к `AttributeError`. Если планируется проверка подписи, следует добавить переменную (напр. `JWT_PUBLIC_KEY`) в конфиг. +- **`brusnika-stage`/`brusnika-prod`: `image.name` указывает на `documentations` (`…/documentations:prod_5904312b`), а не на `measurements`** — вероятно скопировано из другого сервиса; для measurements образ должен указывать на `…/measurements`. +- **Проверки liveness/readiness отключены** во всех манифестах (`probes.*.enabled: false`); HTTP-эндпоинта healthcheck у сервиса нет. +- **Probes/порт в brusnika-оверлеях:** `deployment.port` задан `8080`, тогда как контейнер (`entrypoint.sh` → gunicorn) слушает `8000`, и `service.port`/`targetPort` = `8000`. + +--- + +## Минимальный набор для локального запуска + +- `S3_JSON_SETTINGS` (обязателен) — реальные доступы к S3/MinIO с бакетом(ами) растров. +- при необходимости: `LOG_LEVEL`, `TRACING_USE` (+ `TRACING_*`), `USE_SENTRY` (+ `SENTRY_*`), `AUTH` (+ `DJANGO_*`). + +Сервис слушает `0.0.0.0:8000` (gunicorn, 4 воркера uvicorn). См. пример значений в `.env.example`. diff --git a/apps/measurements/openapi.json b/apps/measurements/openapi.json new file mode 100644 index 0000000..1bf5cff --- /dev/null +++ b/apps/measurements/openapi.json @@ -0,0 +1,927 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "measurements", + "description": "HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO через GDAL.\n\nВсе эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `:<путь/к/файлу.tif>`; система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе координат растра.\n\nАутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.", + "version": "0.0.1" + }, + "paths": { + "/api/point": { + "post": { + "summary": "Height/temperature at a single point", + "description": "Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3.", + "operationId": "point_api_point_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffPoint" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object", + "title": "Response Point Api Point Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/points": { + "post": { + "summary": "Height/temperature at multiple points", + "description": "Батч-версия /point: массив высот/температур для списка точек.", + "operationId": "points_api_points_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffPoints" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "type": "array", + "title": "Response Points Api Points Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/profile": { + "post": { + "summary": "Elevation profile along a polyline", + "description": "Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки.", + "operationId": "profile_api_profile_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffProfile" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "type": "array", + "title": "Response Profile Api Profile Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/volume": { + "post": { + "summary": "Volume inside a polygon", + "description": "Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек).", + "operationId": "volume_api_volume_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffVolume" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "additionalProperties": { + "type": "number" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Response Volume Api Volume Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/multiple": { + "post": { + "summary": "Volume difference between two DEMs", + "description": "Разница объёмов между master- и slave-растром в пределах полигона.", + "operationId": "multiple_api_multiple_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TiffMultiple" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "number" + }, + "type": "object", + "title": "Response Multiple Api Multiple Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/transform": { + "post": { + "summary": "GeoTIFF affine transform", + "description": "Возвращает 6 коэффициентов GDAL GeoTransform растра.", + "operationId": "transform_api_transform_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 6, + "minItems": 6, + "title": "Response Transform Api Transform Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/info": { + "post": { + "summary": "GeoTIFF metadata", + "description": "Метаданные растра (размер, проекция, geotransform и т.п.).", + "operationId": "info_api_info_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Response Info Api Info Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/statistics": { + "post": { + "summary": "Raster min/max statistics", + "description": "Пара (min, max) значений растра.", + "operationId": "statistics_api_statistics_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2, + "title": "Response Statistics Api Statistics Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/bounds": { + "post": { + "summary": "Tile bounds by zoom range", + "description": "Границы тайлов (XYZ) для диапазона zoom_from..zoom_to.", + "operationId": "bounds_api_bounds_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tiff" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "items": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "type": "array" + }, + "type": "object", + "title": "Response Bounds Api Bounds Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Tiff": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "zoom_to": { + "type": "integer", + "title": "Zoom To", + "default": 21 + }, + "zoom_from": { + "type": "integer", + "title": "Zoom From", + "default": 14 + } + }, + "type": "object", + "required": [ + "path" + ], + "title": "Tiff" + }, + "TiffMultiple": { + "properties": { + "master_path": { + "type": "string", + "title": "Master Path" + }, + "slave_path": { + "type": "string", + "title": "Slave Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "master_path", + "slave_path", + "points" + ], + "title": "TiffMultiple", + "example": [ + { + "master_path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 37.34743572357015, + 55.68881158675471 + ], + [ + 37.347128864435845, + 55.6888704629146 + ], + [ + 37.34719182945381, + 55.68896771656465 + ], + [ + 37.34732057806979, + 55.688942063559054 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "slave_path": "geotiff_dems/NTG030521_DEM.tif" + } + ] + }, + "TiffPoint": { + "properties": { + "altitude_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Altitude Path" + }, + "temperature_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Temperature Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "point": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2, + "title": "Point" + } + }, + "type": "object", + "required": [ + "point" + ], + "title": "TiffPoint", + "example": [ + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "point": [ + 59.95821631734607, + 57.9660901731621 + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "point": [ + 1494214.348999979, + 516505.3900003205 + ], + "proj": "", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "point": [ + 1494214.348999979, + 516505.3900003205 + ], + "proj": "" + } + ] + }, + "TiffPoints": { + "properties": { + "altitude_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Altitude Path" + }, + "temperature_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Temperature Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "points" + ], + "title": "TiffPoints", + "example": [ + { + "altitude_path": "geotiff_dems/ALTITUDE_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ], + [ + 59.95563056285039, + 57.966613723012216 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + }, + { + "points": [ + [ + 1494221.1549999786, + 516495.86600015266 + ], + [ + 1494214.348999979, + 516505.3900003205 + ] + ], + "proj": "", + "temperature_path": "geotiff_dems/THERMAL_DEM.tif" + } + ] + }, + "TiffProfile": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + } + }, + "type": "object", + "required": [ + "path", + "points" + ], + "title": "TiffProfile", + "example": [ + { + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs" + }, + { + "path": "geotiff_dems/BLG_080122_DEM.tif", + "points": [ + [ + 3334617.713961677, + 590691.7743950449 + ], + [ + 3334977.6348458366, + 590688.296080064 + ] + ] + } + ] + }, + "TiffVolume": { + "properties": { + "path": { + "type": "string", + "title": "Path" + }, + "proj": { + "type": "string", + "title": "Proj", + "default": "" + }, + "level": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Level" + }, + "points": { + "items": { + "prefixItems": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array", + "maxItems": 2, + "minItems": 2 + }, + "type": "array", + "title": "Points" + }, + "mode": { + "allOf": [ + { + "$ref": "#/components/schemas/VolumeMode" + } + ], + "default": "cv2" + } + }, + "type": "object", + "required": [ + "path", + "points" + ], + "title": "TiffVolume", + "example": [ + { + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 59.95821631734607, + 57.9660901731621 + ], + [ + 59.95452603553467, + 57.96567474229709 + ], + [ + 59.95563056285039, + 57.966613723012216 + ] + ], + "proj": "+proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs" + }, + { + "mode": "pillow", + "path": "geotiff_dems/NTG030521_DEM.tif", + "points": [ + [ + 1494221.1549999786, + 516495.86600015266 + ], + [ + 1494214.348999979, + 516505.3900003205 + ] + ], + "proj": "" + } + ] + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "VolumeMode": { + "type": "string", + "enum": [ + "cv2", + "pillow" + ], + "title": "VolumeMode" + } + } + } +} \ No newline at end of file diff --git a/apps/measurements/openapi.yaml b/apps/measurements/openapi.yaml new file mode 100644 index 0000000..627f629 --- /dev/null +++ b/apps/measurements/openapi.yaml @@ -0,0 +1,565 @@ +openapi: 3.1.0 +info: + title: measurements + description: 'HTTP-сервис измерений по GeoTIFF-растрам (DEM/thermal), читаемым напрямую из S3/MinIO + через GDAL. + + + Все эндпоинты — POST под префиксом `/api`. `path` в теле запроса указывается в формате `:<путь/к/файлу.tif>`; + система координат задаётся строкой `proj` (proj4). Пустой `proj` означает, что точки уже в системе + координат растра. + + + Аутентификация (JWT в заголовках `authorization`/`identity`) включается переменной `AUTH=1`.' + version: 0.0.1 +paths: + /api/point: + post: + summary: Height/temperature at a single point + description: Возвращает высоту (h) и/или температуру (t) в одной точке по DEM/термо-растру из S3. + operationId: point_api_point_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffPoint' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + title: Response Point Api Point Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/points: + post: + summary: Height/temperature at multiple points + description: 'Батч-версия /point: массив высот/температур для списка точек.' + operationId: points_api_points_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffPoints' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + type: array + title: Response Points Api Points Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/profile: + post: + summary: Elevation profile along a polyline + description: Профиль высот вдоль ломаной; между вершинами добавляются промежуточные точки. + operationId: profile_api_profile_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffProfile' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + additionalProperties: + anyOf: + - type: number + - type: 'null' + type: object + type: array + title: Response Profile Api Profile Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/volume: + post: + summary: Volume inside a polygon + description: Объём внутри полигона. mode=cv2 (по умолчанию) или pillow (DEM-режим, требует > 2 точек). + operationId: volume_api_volume_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffVolume' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + anyOf: + - additionalProperties: + type: number + type: object + - type: 'null' + title: Response Volume Api Volume Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/multiple: + post: + summary: Volume difference between two DEMs + description: Разница объёмов между master- и slave-растром в пределах полигона. + operationId: multiple_api_multiple_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TiffMultiple' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: number + type: object + title: Response Multiple Api Multiple Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/transform: + post: + summary: GeoTIFF affine transform + description: Возвращает 6 коэффициентов GDAL GeoTransform растра. + operationId: transform_api_transform_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + prefixItems: + - type: number + - type: number + - type: number + - type: number + - type: number + - type: number + type: array + maxItems: 6 + minItems: 6 + title: Response Transform Api Transform Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/info: + post: + summary: GeoTIFF metadata + description: Метаданные растра (размер, проекция, geotransform и т.п.). + operationId: info_api_info_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Info Api Info Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/statistics: + post: + summary: Raster min/max statistics + description: Пара (min, max) значений растра. + operationId: statistics_api_statistics_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + title: Response Statistics Api Statistics Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/bounds: + post: + summary: Tile bounds by zoom range + description: Границы тайлов (XYZ) для диапазона zoom_from..zoom_to. + operationId: bounds_api_bounds_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Tiff' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + items: + items: + type: integer + type: array + type: array + type: object + title: Response Bounds Api Bounds Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + Tiff: + properties: + path: + type: string + title: Path + zoom_to: + type: integer + title: Zoom To + default: 21 + zoom_from: + type: integer + title: Zoom From + default: 14 + type: object + required: + - path + title: Tiff + TiffMultiple: + properties: + master_path: + type: string + title: Master Path + slave_path: + type: string + title: Slave Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - master_path + - slave_path + - points + title: TiffMultiple + example: + - master_path: geotiff_dems/NTG030521_DEM.tif + points: + - - 37.34743572357015 + - 55.68881158675471 + - - 37.347128864435845 + - 55.6888704629146 + - - 37.34719182945381 + - 55.68896771656465 + - - 37.34732057806979 + - 55.688942063559054 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + slave_path: geotiff_dems/NTG030521_DEM.tif + TiffPoint: + properties: + altitude_path: + anyOf: + - type: string + - type: 'null' + title: Altitude Path + temperature_path: + anyOf: + - type: string + - type: 'null' + title: Temperature Path + proj: + type: string + title: Proj + default: '' + point: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + title: Point + type: object + required: + - point + title: TiffPoint + example: + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + point: + - 59.95821631734607 + - 57.9660901731621 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + temperature_path: geotiff_dems/THERMAL_DEM.tif + - point: + - 1494214.348999979 + - 516505.3900003205 + proj: '' + temperature_path: geotiff_dems/THERMAL_DEM.tif + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + point: + - 1494214.348999979 + - 516505.3900003205 + proj: '' + TiffPoints: + properties: + altitude_path: + anyOf: + - type: string + - type: 'null' + title: Altitude Path + temperature_path: + anyOf: + - type: string + - type: 'null' + title: Temperature Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - points + title: TiffPoints + example: + - altitude_path: geotiff_dems/ALTITUDE_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + - - 59.95563056285039 + - 57.966613723012216 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + temperature_path: geotiff_dems/THERMAL_DEM.tif + - points: + - - 1494221.1549999786 + - 516495.86600015266 + - - 1494214.348999979 + - 516505.3900003205 + proj: '' + temperature_path: geotiff_dems/THERMAL_DEM.tif + TiffProfile: + properties: + path: + type: string + title: Path + proj: + type: string + title: Proj + default: '' + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + type: object + required: + - path + - points + title: TiffProfile + example: + - path: geotiff_dems/NTG030521_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + - path: geotiff_dems/BLG_080122_DEM.tif + points: + - - 3334617.713961677 + - 590691.7743950449 + - - 3334977.6348458366 + - 590688.296080064 + TiffVolume: + properties: + path: + type: string + title: Path + proj: + type: string + title: Proj + default: '' + level: + anyOf: + - type: number + - type: 'null' + title: Level + points: + items: + prefixItems: + - type: number + - type: number + type: array + maxItems: 2 + minItems: 2 + type: array + title: Points + mode: + allOf: + - $ref: '#/components/schemas/VolumeMode' + default: cv2 + type: object + required: + - path + - points + title: TiffVolume + example: + - path: geotiff_dems/NTG030521_DEM.tif + points: + - - 59.95821631734607 + - 57.9660901731621 + - - 59.95452603553467 + - 57.96567474229709 + - - 59.95563056285039 + - 57.966613723012216 + proj: +proj=tmerc +lat_0=0 +lon_0=60.05 +k=1 +x_0=1500000 +y_0=-5911057.63 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 + +units=m +no_defs + - mode: pillow + path: geotiff_dems/NTG030521_DEM.tif + points: + - - 1494221.1549999786 + - 516495.86600015266 + - - 1494214.348999979 + - 516505.3900003205 + proj: '' + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError + VolumeMode: + type: string + enum: + - cv2 + - pillow + title: VolumeMode diff --git a/apps/subscriptions/.env.example b/apps/subscriptions/.env.example new file mode 100644 index 0000000..4edb925 --- /dev/null +++ b/apps/subscriptions/.env.example @@ -0,0 +1,64 @@ +# ============================================================================ +# sarex-subscriptions — пример переменных окружения (api + cron-задачи) +# +# Django-сервис уведомлений: HTTP-API (uwsgi) + две CronJob-задачи рассылки +# (run_notifications / run_immediately_notifications). Все компоненты +# используют один модуль настроек config.settings.production и общий набор +# переменных. +# +# Скопируйте нужные значения в config.env / .env соответствующего окружения. +# Переменные, помеченные (обяз.), обязательны для работы — без них процесс +# упадёт при старте либо при обращении к соответствующему сервису. +# bool в Django читается как строка: непустая строка = True (см. замечания). +# ============================================================================ + +# --- База данных PostgreSQL/PostGIS (api + cron) ---------------------------- +# Движок — core.db.backends.postgis (GeoDjango), нужен PostGIS. +DATABASE_HOST=127.0.0.1 # (обяз.) хост PostgreSQL +DATABASE_PORT=5432 # (обяз.) порт PostgreSQL +DATABASE_NAME=subscriptions # (обяз.) имя базы данных +DATABASE_USER=user # (обяз.) пользователь БД +DATABASE_PASSWORD=password # (обяз.) пароль пользователя БД + +# --- Хранилище S3 / MinIO (api: static + media) ----------------------------- +# STATICFILES_STORAGE и DEFAULT_FILE_STORAGE = S3Boto3Storage, поэтому креды +# фактически обязательны для корректной работы статики/медиа. +YC_S3_ACCESS_KEY_ID=access_key # (обяз.) access key +YC_S3_SECRET_ACCESS_KEY=secret_key # (обяз.) secret key +YC_S3_BUCKET_NAME=subscriptions # (обяз.) имя бакета +YC_S3_ENDPOINT_URL=https://storage.yandexcloud.net # (обяз.) endpoint S3 + +# --- Внешние сервисы (в основном cron-рассылки) ----------------------------- +SYSTEM_LOG_HOST=http://localhost:8888 # (обяз.) URL сервиса system-log +USER_SERVICE_HOST=http://localhost:8000 # (обяз.) URL сервиса пользователей (Django/ЛК) +USER_SERVICE_LOGIN=superuser # логин для авторизации в user-service (по умолч. "") +USER_SERVICE_PASSWORD=password # пароль для авторизации в user-service (по умолч. "") +ALLOWED_HOST_EMAIL=https://lk.sarex.io # базовый URL для ссылок в письмах (по умолч. https://lk.sarex.io) + +# --- Email через Mailgun (cron) --------------------------------------------- +IS_MAILGUN_USE=0 # использовать Mailgun (по умолч. True; 0 — выключить) +MAILGUN_API_KEY= # API-ключ Mailgun (по умолч. "") +# MAILGUN_BASE_URL=https://api.mailgun.net/v3/mg.sarex.io # базовый URL Mailgun API +# MAILGUN_EMAIL_FROM=hello@sarex.io # адрес отправителя + +# --- Email через SMTP (cron; используется если задан хост) ------------------ +SMTP_EMAIL_HOST= # хост SMTP (по умолч. None — SMTP отключён) +SMTP_EMAIL_PORT= # порт SMTP (по умолч. None) +# SMTP_EMAIL_FROM=hello@sarex.io # адрес отправителя (по умолч. hello@sarex.io) + +# --- Telegram (cron) -------------------------------------------------------- +IS_USE_TELEGRAM=false # использовать Telegram (по умолч. True; см. замечание про bool) +TELEGRAM_BOT_TOKEN= # токен бота (по умолч. "") + +# --- Трейсинг OpenTelemetry (api; необязательно) ---------------------------- +# Блок включается, если USE_OTEL задана НЕПУСТОЙ строкой (даже "False" → вкл!). +# USE_OTEL=True # включить OTEL-трейсинг и otel-логгер +# SERVICE_NAME=subscriptions # имя сервиса в трейсах (по умолч. subscriptions) +# TRACER_ENDPOINT=localhost:4375 # адрес OTLP-коллектора (по умолч. localhost:4375) +# USE_INSECURE=True # подключение без TLS (та же логика bool, что и USE_OTEL) + +# --- Инфраструктурные / вспомогательные (кодом приложения НЕ читаются) ------- +# API_ADDRESS=8000 # задаётся в манифестах; порт uwsgi фиксирован в uwsgi.ini (0.0.0.0:8000) +# DJANGO_SETTINGS_MODULE=config.settings.production # задаётся в entrypoint.sh / wsgi.py +# NEXUS_USERNAME= # build-arg Dockerfile: доступ к приватному PyPI (nexus.infra.sarex.io) +# NEXUS_PASSWORD= # build-arg Dockerfile: пароль к приватному PyPI diff --git a/apps/subscriptions/CONFIGURATION.md b/apps/subscriptions/CONFIGURATION.md new file mode 100644 index 0000000..3b440cb --- /dev/null +++ b/apps/subscriptions/CONFIGURATION.md @@ -0,0 +1,200 @@ +# Конфигурация sarex-subscriptions (api + cron-задачи) + +Документ описывает все переменные окружения и способы конфигурирования сервиса `sarex-subscriptions` — Django-приложения рассылки уведомлений (email/Telegram) по подпискам. + +Компоненты сервиса используют **один и тот же** модуль настроек `config.settings.production` и общий набор переменных окружения: + +- **api** — HTTP-сервис (uwsgi, `config.wsgi:application`), REST API подписок/получателей/шаблонов; отдаёт статику и медиа через S3; +- **cron `run_notifications`** (`subscription-periodic`) — периодическая рассылка отложенных уведомлений; +- **cron `run_immediately_notifications`** (`subscription-immediately`) — рассылка немедленных уведомлений. + +Обе cron-задачи — это management-команды Django (`server/apps/notification/management/commands`), запускаемые как отдельные `CronJob` из того же образа. + +## Способы конфигурирования + +Сервис настраивается **через переменные окружения** (`os.getenv` в `config/settings/base.py` и `config/settings/production.py`) плюс жёстко заданные в коде настройки. Отдельной библиотеки разбора конфига (как cleanenv в Go-сервисах) здесь нет — используется штатный механизм Django settings. + +Часть настроек **захардкожена** в `base.py` и не выносится в окружение: `SECRET_KEY`, `DEBUG`, `ALLOWED_HOSTS`, `CORS_*`, REST Framework, локаль/таймзона (`ru-ru`, `Europe/Moscow`). В `production.py` `DEBUG=False` и заданы фиксированные `ALLOWED_HOSTS`/`CORS_ALLOWED_ORIGINS`. + +Обязательные переменные не помечены тегами (как в Go), но при их отсутствии процесс падает при старте (подключение к БД) либо при обращении к соответствующему сервису (клиенты system-log/user-service, хранилище S3). + +Источники переменных по способам запуска: + +| Способ запуска | Откуда берутся переменные | +| --- | --- | +| Локально (docker-compose) | `docker-compose.yml` поднимает контейнер `db` (`postgis/postgis`) и сервисы `api`/`migrations`. Значения БД (`DATABASE_USER/PASSWORD/NAME`, `ALLOWED_HOST_EMAIL`) подставляются из окружения/файла `.env` рядом с compose. api собирается из `compose/server/Dockerfile` и стартует через `entrypoint.sh` | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml`: зависимость от `universal-chart`. Блоки `services.api.envs` (обычные значения, per-env `_default/stage/preprod/production`) и `services.api.secretEnvs` (из k8s-секретов). Плюс `cronjobs.periodic` / `cronjobs.immediately` — шаблоны `CronJob` в `.helm/templates/`, наследующие `envs`/`secretEnvs` api | +| Kubernetes — этот infra-репозиторий (`iac/apps/subscriptions`) | `base/` — kustomize-манифесты с инъекцией секретов через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + postgresql), `brusnika-stage` / `brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блоки `envs`/`secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`, ref `apps-business`); в `workflow.rules` задаются переменные пайплайна (`SERVICE_NAME`, `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME` и т.п.) | + +**Миграции БД (api).** Выполняются **в entrypoint контейнера api** перед стартом uwsgi: `python manage.py migrate --settings=config.settings.production` (`compose/server/entrypoint.sh`). Cron-задачи миграций не выполняют. Локально в `docker-compose.yml` отдельный сервис `migrations` вызывает `makemigrations` (генерация миграций, не применение). + +--- + +## api (`sarex-subscriptions`) + +Переменные читаются в `config/settings/base.py` (S3, OTEL) и `config/settings/production.py` (БД и внешние сервисы; `production.py` импортирует всё из `base.py`). + +### База данных (PostgreSQL / PostGIS) + +Движок — `core.db.backends.postgis` (GeoDjango, требуется PostGIS). Все пять переменных обязательны — без них подключение к БД не поднимется. + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `DATABASE_HOST` | string | да | — | Хост PostgreSQL | +| `DATABASE_PORT` | string | да | — | Порт PostgreSQL | +| `DATABASE_NAME` | string | да | — | Имя базы данных | +| `DATABASE_USER` | string | да | — | Пользователь БД | +| `DATABASE_PASSWORD` | string | да | — | Пароль пользователя БД | + +### Хранилище S3 (static + media) + +`STATICFILES_STORAGE` и `DEFAULT_FILE_STORAGE` = `storages.backends.s3boto3.S3Boto3Storage`, `AWS_DEFAULT_ACL="public-read"`. Значения по умолчанию отсутствуют (`os.getenv` без default → `None`), поэтому для корректной отдачи статики/медиа креды фактически обязательны. + +| Переменная | Тип | Обяз. | Назначение | +| --- | --- | --- | --- | +| `YC_S3_ACCESS_KEY_ID` | string | да | Access key (`AWS_ACCESS_KEY_ID`) | +| `YC_S3_SECRET_ACCESS_KEY` | string | да | Secret key (`AWS_SECRET_ACCESS_KEY`) | +| `YC_S3_BUCKET_NAME` | string | да | Имя бакета (`AWS_STORAGE_BUCKET_NAME`) | +| `YC_S3_ENDPOINT_URL` | string | да | Endpoint S3 (`AWS_S3_ENDPOINT_URL`) | + +### Трейсинг (OpenTelemetry) + +Блок OTEL в `base.py` (и обёртка WSGI в `wsgi.py`) включается по `os.getenv('USE_OTEL', False)`. **Важно:** проверяется истинность строки, а не её значение — любая непустая строка (в т.ч. `"False"`, `"0"`) включает трейсинг. Задействует пакет `django_otel_tools`. + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `USE_OTEL` | bool-строка | не задана (выкл.) | Включает OTEL-трейсинг, otel-логгер и `OtelMiddleware` | +| `SERVICE_NAME` | string | `subscriptions` | Имя сервиса в трейсах | +| `TRACER_ENDPOINT` | string | `localhost:4375` | Адрес OTLP-коллектора | +| `USE_INSECURE` | bool-строка | не задана (выкл.) | Небезопасное (без TLS) подключение к коллектору; та же логика истинности строки, что и `USE_OTEL` | + +> Захардкожено (не через окружение): `SECRET_KEY`, `DEBUG` (`False` в production), `ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CORS_ALLOW_ALL_ORIGINS=True`. + +--- + +## cron-задачи (`run_notifications`, `run_immediately_notifications`) + +Обе команды используют тот же модуль настроек и, помимо БД, обращаются к внешним сервисам для сбора данных и рассылки. Переменные читаются в `production.py` и потребляются в `server/apps/notification/management/commands/*.py`. + +### Внешние сервисы и рассылка + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SYSTEM_LOG_HOST` | string | да | — | Базовый URL сервиса system-log (логирование событий) | +| `USER_SERVICE_HOST` | string | да | — | Базовый URL сервиса пользователей (Django/ЛК) | +| `USER_SERVICE_LOGIN` | string | нет | `""` | Логин для авторизации в user-service | +| `USER_SERVICE_PASSWORD` | string | нет | `""` | Пароль для авторизации в user-service | +| `ALLOWED_HOST_EMAIL` | string | нет | `https://lk.sarex.io` | Базовый URL для формирования ссылок в письмах (`context_processing/document.py`) | + +### Email — Mailgun + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `IS_MAILGUN_USE` | bool-строка | нет | `True` | Использовать Mailgun для отправки писем | +| `MAILGUN_API_KEY` | string | нет | `""` | API-ключ Mailgun | +| `MAILGUN_BASE_URL` | string | нет | `https://api.mailgun.net/v3/mg.sarex.io` | Базовый URL Mailgun API | +| `MAILGUN_EMAIL_FROM` | string | нет | `hello@sarex.io` | Адрес отправителя | + +### Email — SMTP + +SMTP-ветка активируется, только если **заданы оба** `SMTP_EMAIL_HOST` и `SMTP_EMAIL_PORT` (по умолчанию `None` — SMTP отключён). + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `SMTP_EMAIL_HOST` | string | нет | `None` | Хост SMTP-сервера | +| `SMTP_EMAIL_PORT` | string | нет | `None` | Порт SMTP-сервера | +| `SMTP_EMAIL_FROM` | string | нет | `hello@sarex.io` | Адрес отправителя | + +### Telegram + +| Переменная | Тип | Обяз. | По умолчанию | Назначение | +| --- | --- | --- | --- | --- | +| `IS_USE_TELEGRAM` | bool-строка | нет | `True` | Использовать Telegram-рассылку | +| `TELEGRAM_BOT_TOKEN` | string | нет | `""` | Токен Telegram-бота (`settings.TELEGRAM_BOT_TOKEN`) | + +> Значения `IS_*` читаются как строки Django-настроек: непустая строка истинна. Чтобы выключить канал, инфраструктурные манифесты задают `"false"`/`"0"` — но с точки зрения Python это тоже непустые строки, поэтому фактическое поведение зависит от того, как значение интерпретируется в коде команды (см. замечания). + +--- + +## Инфраструктурные и вспомогательные переменные + +Не читаются кодом приложения, но участвуют в запуске/сборке/деплое. + +| Переменная | Где используется | Назначение | +| --- | --- | --- | +| `API_ADDRESS` | `base/*.yaml`, `.helm/values.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом не читается** — порт uwsgi фиксирован в `uwsgi.ini` (`http = 0.0.0.0:8000`) | +| `DJANGO_SETTINGS_MODULE` | `entrypoint.sh`, `wsgi.py`, `manage.py` | Модуль настроек Django (`config.settings.production` в проде) | +| `NEXUS_USERNAME`, `NEXUS_PASSWORD` | `compose/server/Dockerfile` (build-arg), `.gitlab-ci.yml` | Доступ к приватному PyPI (`nexus.infra.sarex.io`) при сборке образа | +| `SERVICE_NAME`, `DOCKERFILE_PATH`, `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` (`variables`) | Параметры сборки/пайплайна | +| `STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `K8S_HUSTLER_BRANCH`, `HELM_SET_ARGS`, `IMAGE_NAME`, `ENABLE_BUILD_IMAGE` | `.gitlab-ci.yml` (`workflow.rules`) | Параметры пайплайна `generic/common-ci` (деплой по окружениям stage/preprod/production) | + +> Обратите внимание: `SERVICE_NAME` встречается в двух ролях — как переменная приложения (имя сервиса в OTEL-трейсах) и как переменная CI (имя сервиса для сборки/чарта). Значения задаются в разных местах и не связаны между собой. + +--- + +## Деплой из этого репозитория (`iac/apps/subscriptions`) + +Здесь используется **kustomize** (не собственный Helm-чарт сервиса). Секреты БД и S3 инъектируются агентом **Vault** и подгружаются в окружение процесса до старта: + +``` +set -a +[ -f /vault/secrets/subscriptions-postgresql ] && . /vault/secrets/subscriptions-postgresql +[ -f /vault/secrets/subscriptions-minio ] && . /vault/secrets/subscriptions-minio +set +a +exec /server/entrypoint.sh +``` + +### `base/` + +- `backend-deployment.yaml` — обычные переменные инлайн в `env:` (`API_ADDRESS`, `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST`, `IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`, `SMTP_EMAIL_FROM/HOST/PORT`). Vault-шаблоны формируют `DATABASE_HOST/PORT/NAME/USER/PASSWORD` (из `secrets/data/postgresql/apps/subscriptions`, БД `subscriptions_db`) и `YC_S3_ACCESS_KEY_ID/SECRET_ACCESS_KEY/BUCKET_NAME/ENDPOINT_URL` (из `secrets/data/minio/apps/subscriptions`). +- `backend-service.yaml`, `namespace.yaml` (`istio-injection: enabled`), `serviceaccount.yaml` (`subscriptions-vault`). +- `kustomization.yaml` собирает namespace, serviceaccount, deployment и service. + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + `postgresql.yaml` (HelmRelease `postgresql-contour`: создаёт БД `subscriptions_db`, пользователя `subscriptions`, расширения `ltree`/`pg_stat_statements`/`postgis`/`timescaledb`, восстановление из дампа). +- **`brusnika-stage`** / **`brusnika-prod`** — Flux `HelmRelease` на `universal-chart` (v0.1.7). Переменные — в `envs` (`DATABASE_HOST/PORT/NAME`, `API_ADDRESS`, `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST`, `IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`, `SMTP_*`), секреты — в `secretEnvs` (`postgres-secret`: `username`/`password`; `yc-s3-secret`: `key_id`/`access_key`/`storage_bucket_name`/`endpoint_url`). Отличаются только `DATABASE_HOST` (`postgres-service` в prod vs `192.168.2.45` в stage). + +> В этих kustomize/brusnika-манифестах **не задаются** `USE_OTEL`, `TELEGRAM_BOT_TOKEN`, `MAILGUN_API_KEY`, `USER_SERVICE_LOGIN/PASSWORD`, `ALLOWED_HOST_EMAIL` — используются дефолты из кода. Telegram и Mailgun выключены (`IS_USE_TELEGRAM=false`, `IS_MAILGUN_USE=0`), рассылка идёт по SMTP. + +--- + +## Отличия от Helm-чарта репозитория (`.helm/values.yaml`) + +Собственный чарт сервиса (`.helm/`) — это **другой** путь деплоя, с более широким набором переменных, чем kustomize/brusnika здесь: + +- дополнительно задаёт `ALLOWED_HOST_EMAIL`, `USE_OTEL=True`, `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE=True` (per-env), а также секреты `MAILGUN_API_KEY` (`mailgun-cred`) и `TELEGRAM_BOT_TOKEN` (`telegram-bot-secret`); +- `IS_USE_TELEGRAM=true`, монтирует `uwsgi.ini` через ConfigMap; +- определяет `CronJob` `subscription-periodic` (`0,30 * * * *`, `run_notifications`) и `subscription-immediately` (`* * * * *`, `run_immediately_notifications`), наследующие `envs`/`secretEnvs` api. + +--- + +## Замечания и потенциальные проблемы + +- **`manage.py` указывает на несуществующий модуль настроек.** По умолчанию `manage.py` задаёт `DJANGO_SETTINGS_MODULE=config.settings.local`, но модуля `local.py` в репозитории нет (есть только `base.py` и `production.py`). Поэтому management-команды нужно запускать с явным `--settings=config.settings.production` (как это и делают entrypoint и cron-задачи). Локальный сервис `migrations` в `docker-compose.yml` вызывает `makemigrations` **без** `--settings` — команда упадёт из-за отсутствия `local`. +- **`USE_OTEL`/`USE_INSECURE` работают по истинности строки.** `os.getenv('USE_OTEL', False)` возвращает строку; любое непустое значение (включая `"False"`, `"0"`) включает трейсинг. Чтобы выключить — переменную нужно **не задавать вовсе**, а не ставить `False`. +- **Каналы рассылки `IS_MAILGUN_USE` / `IS_USE_TELEGRAM` — тоже строки.** Значения по умолчанию — `True` (Python-объект), но из окружения приходит строка; поведение зависит от того, как значение проверяется в коде команды. При настройке важно учитывать это (в манифестах используют `"false"`/`"0"`). +- **S3 обязателен для статики/медиа.** Хранилища заданы как S3 без файлового фолбэка; при отсутствии `YC_S3_*` operations со статикой/медиа будут падать, хотя сам процесс поднимется. +- **`SECRET_KEY` захардкожен** в `base.py` и не выносится в окружение — для продакшена это стоит вынести в секрет. +- **Расхождения имён БД между окружениями:** infra `base` и postgresql-чарт используют `subscriptions_db`, brusnika-оверлеи — `subscriptions`. При подключении важно использовать значение конкретного окружения. +- **Два деплой-пути расходятся по набору переменных** (kustomize/brusnika vs `.helm`): OTEL, Telegram-токен, Mailgun-ключ и `ALLOWED_HOST_EMAIL` присутствуют только в `.helm`. Это стоит учитывать при переносе окружения. + +--- + +## Минимальный набор для запуска + +**api** (uwsgi, миграции при старте): + +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `YC_S3_ACCESS_KEY_ID`, `YC_S3_SECRET_ACCESS_KEY`, `YC_S3_BUCKET_NAME`, `YC_S3_ENDPOINT_URL` +- при необходимости — `USE_OTEL` (+ `SERVICE_NAME`, `TRACER_ENDPOINT`, `USE_INSECURE`) + +**cron `run_notifications` / `run_immediately_notifications`**: + +- блок БД (как у api) +- `SYSTEM_LOG_HOST`, `USER_SERVICE_HOST` (+ `USER_SERVICE_LOGIN`, `USER_SERVICE_PASSWORD` при авторизации) +- канал рассылки: `IS_MAILGUN_USE` + `MAILGUN_API_KEY` **или** `SMTP_EMAIL_HOST` + `SMTP_EMAIL_PORT`; при Telegram — `IS_USE_TELEGRAM` + `TELEGRAM_BOT_TOKEN` +- при необходимости — `ALLOWED_HOST_EMAIL` (ссылки в письмах) + +См. пример значений в `.env.example` рядом с этим файлом. diff --git a/apps/subscriptions/openapi.yaml b/apps/subscriptions/openapi.yaml new file mode 100644 index 0000000..4c3fc11 --- /dev/null +++ b/apps/subscriptions/openapi.yaml @@ -0,0 +1,719 @@ +openapi: 3.0.3 + +info: + title: sarex-subscriptions API + version: "1.0.0" + description: | + REST API сервиса **sarex-subscriptions** — управление подписками на уведомления, + получателями, шаблонами и просмотр истории рассылок (события, транзакции, сообщения). + + Сервис на Django REST Framework. Роутинг — `rest_framework.routers.DefaultRouter`, + все ресурсы смонтированы под префиксом `/api/v1/` (`config/urls.py`). + + ### Аутентификация + Настроены `BasicAuthentication` и `SessionAuthentication` + (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`). `DEFAULT_PERMISSION_CLASSES` + не заданы, поэтому по умолчанию действует `AllowAny` — эндпоинты доступны без + авторизации, а Basic-креды используются опционально. + + ### Пагинация + `LimitOffsetPagination`, `PAGE_SIZE = 1000`. Списки возвращают объект + `{ count, next, previous, results }`. Управление — query-параметрами `limit` и `offset`. + + ### Фильтрация + Бэкенд фильтров — `DjangoFilterBackend`. Ряд полей принимают список значений + через запятую (кастомный `CustomFilterList`, lookup `in`). + + ### Замечания (расхождения кода) + - `GET /transaction/{id}/` (retrieve) не имеет сериализатора в + `serializer_class_by_action` (есть ключи `list` и `get`, но не `retrieve`) — + поведение отдельного объекта может отличаться от списка. + - `MessageRetrieveSerializer` объявляет поле `created_at`, которого нет в модели + `Message` — поле показано в схеме как в коде сериализатора, но фактически может + приводить к ошибке. + - `RecipientWriteSerializer.extra_kwargs` ссылается на `telegram_user_name`, тогда как + в модели поле называется `telegram_username`. + +servers: + - url: https://api.sarex.io/api/v1 + description: Production (через ЛК) + - url: https://stage-api.sarex.io/api/v1 + description: Stage + - url: http://sarex-subscriptions-service.subscriptions-prod/api/v1 + description: Внутрикластерный адрес (ClusterIP) + +tags: + - name: subscription + description: Подписки на уведомления + - name: recipient + description: Получатели уведомлений + - name: event + description: События уведомлений + - name: template + description: Шаблоны уведомлений + - name: transaction + description: Транзакции рассылки + - name: message + description: Отправленные сообщения + +security: + - basicAuth: [] + - {} + +paths: + # ========================================================================== + # Subscription + # ========================================================================== + /subscription/: + get: + tags: [subscription] + summary: Список подписок + operationId: listSubscriptions + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: user_id + in: query + description: ID пользователя получателя (фильтр по recipient.user_id) + schema: { type: integer } + - name: company_id + in: query + description: ID компании; список значений через запятую + schema: { type: string } + - name: service_name + in: query + description: Название сервиса; список значений через запятую + schema: { type: string } + - name: instance_id + in: query + description: ID сущности; список значений через запятую + schema: { type: string } + - name: instance_uid + in: query + description: UUID сущности; список значений через запятую + schema: { type: string } + - name: model_name + in: query + description: Название модели (точное совпадение) + schema: { type: string } + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Subscription' } + post: + tags: [subscription] + summary: Создать подписку + description: | + Требуется хотя бы одно из полей `instance_id` / `instance_uid` / + `public_instance_id`. Если подписка с таким `instance_uid`/`instance_id` + (+ `model_name`, `recipient`) уже существует — возвращается существующая. + Если `period` не передан — устанавливается `TwoTimesDay` c `first_time=9:00`, + `second_time=17:00`. + operationId: createSubscription + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SubscriptionWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/Subscription' } + '400': + $ref: '#/components/responses/ValidationError' + + /subscription/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [subscription] + summary: Получить подписку + operationId: retrieveSubscription + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Subscription' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [subscription] + summary: Удалить подписку + operationId: destroySubscription + responses: + '204': { description: No Content } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # Recipient + # ========================================================================== + /recipient/: + get: + tags: [recipient] + summary: Список получателей + operationId: listRecipients + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: user_id + in: query + description: ID пользователя; список значений через запятую + schema: { type: string } + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Recipient' } + post: + tags: [recipient] + summary: Создать получателя + operationId: createRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': + $ref: '#/components/responses/ValidationError' + + /recipient/{user_id}/: + description: | + Поиск объекта выполняется по полю `user_id` (`lookup_field = "user_id"`), + а не по первичному ключу `id`. + parameters: + - name: user_id + in: path + required: true + description: ID пользователя (lookup_field) + schema: { type: integer } + get: + tags: [recipient] + summary: Получить получателя + operationId: retrieveRecipient + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '404': { $ref: '#/components/responses/NotFound' } + put: + tags: [recipient] + summary: Обновить получателя + description: Поля `email`, `user_id`, `id` доступны только на чтение и не изменяются. + operationId: updateRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/NotFound' } + patch: + tags: [recipient] + summary: Частично обновить получателя + operationId: partialUpdateRecipient + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RecipientUpdate' } + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Recipient' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + tags: [recipient] + summary: Удалить получателя + operationId: destroyRecipient + responses: + '204': { description: No Content } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationEvent + # ========================================================================== + /event/: + get: + tags: [event] + summary: Список событий + operationId: listEvents + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationEvent' } + post: + tags: [event] + summary: Зарегистрировать событие + operationId: createEvent + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEventWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEvent' } + '400': { $ref: '#/components/responses/ValidationError' } + + /event/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [event] + summary: Получить событие + operationId: retrieveEvent + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationEvent' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationTemplate + # ========================================================================== + /template/: + get: + tags: [template] + summary: Список шаблонов + operationId: listTemplates + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationTemplate' } + post: + tags: [template] + summary: Создать шаблон + operationId: createTemplate + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplateWrite' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplate' } + '400': { $ref: '#/components/responses/ValidationError' } + + /template/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [template] + summary: Получить шаблон + operationId: retrieveTemplate + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTemplate' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # NotificationTransaction (read-only) + # ========================================================================== + /transaction/: + get: + tags: [transaction] + summary: Список транзакций рассылки + operationId: listTransactions + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/NotificationTransaction' } + + /transaction/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [transaction] + summary: Получить транзакцию + description: > + Внимание: для действия retrieve не задан сериализатор + (`serializer_class_by_action` содержит только `list`/`get`), поведение + одиночного объекта может отличаться. + operationId: retrieveTransaction + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/NotificationTransaction' } + '404': { $ref: '#/components/responses/NotFound' } + + # ========================================================================== + # Message (read-only) + # ========================================================================== + /message/: + get: + tags: [message] + summary: Список сообщений + operationId: listMessages + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedList' + - type: object + properties: + results: + type: array + items: { $ref: '#/components/schemas/Message' } + + /message/{id}/: + parameters: + - $ref: '#/components/parameters/PathId' + get: + tags: [message] + summary: Получить сообщение + operationId: retrieveMessage + responses: + '200': + description: OK + content: + application/json: + schema: { $ref: '#/components/schemas/Message' } + '404': { $ref: '#/components/responses/NotFound' } + +# ============================================================================ +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + + parameters: + Limit: + name: limit + in: query + description: Кол-во объектов на странице (LimitOffsetPagination) + schema: { type: integer, default: 1000 } + Offset: + name: offset + in: query + description: Смещение от начала выборки + schema: { type: integer, minimum: 0 } + PathId: + name: id + in: path + required: true + description: Первичный ключ объекта + schema: { type: integer } + + responses: + NotFound: + description: Объект не найден + content: + application/json: + schema: { $ref: '#/components/schemas/Detail' } + ValidationError: + description: Ошибка валидации + content: + application/json: + schema: + type: object + additionalProperties: true + example: + field_name: ["Обязательное поле."] + + schemas: + # ---- Общие ---- + PaginatedList: + type: object + properties: + count: { type: integer, example: 42 } + next: + type: string + format: uri + nullable: true + example: "http://host/api/v1/subscription/?limit=1000&offset=1000" + previous: + type: string + format: uri + nullable: true + results: + type: array + items: {} + required: [count, results] + + Detail: + type: object + properties: + detail: { type: string, example: "Не найдено." } + + # ---- Перечисления ---- + PeriodType: + type: integer + description: | + Периодичность рассылки: + * 0 — Два раза в день + * 1 — Каждый день (default) + * 2 — Один раз в неделю + * 3 — Сразу при изменении + enum: [0, 1, 2, 3] + default: 1 + + WeekdayType: + type: integer + description: | + День недели: 1 — Пн, 2 — Вт, 3 — Ср, 4 — Чт, 5 — Пт, 6 — Сб, 7 — Вс + enum: [1, 2, 3, 4, 5, 6, 7] + nullable: true + + EventType: + type: integer + description: | + Тип события: 0 — Edit (default), 1 — Create, 2 — Delete, 3 — Copy, 4 — Read + enum: [0, 1, 2, 3, 4] + default: 0 + + MessageServiceType: + type: integer + description: "Способ отправки: 0 — Email (default), 1 — Telegram" + enum: [0, 1] + default: 0 + + TransactionStatus: + type: integer + description: | + Статус транзакции: 0 — Новая, 1 — В обработке, 2 — Успешно, 3 — Ошибка + enum: [0, 1, 2, 3] + + # ---- Subscription ---- + Subscription: + type: object + properties: + id: { type: integer, readOnly: true } + model_name: { type: string, maxLength: 255 } + instance_id: { type: integer, nullable: true } + instance_uid: { type: string, format: uuid, nullable: true } + public_instance_id: { type: string, format: uuid, nullable: true } + company_id: { type: integer } + service_name: { type: string, maxLength: 255, nullable: true } + recipient: + type: integer + description: ID получателя (FK recipient.Recipient) + period: { $ref: '#/components/schemas/PeriodType' } + weekday: { $ref: '#/components/schemas/WeekdayType' } + first_time: { type: string, maxLength: 6, nullable: true, example: "9:00" } + second_time: { type: string, maxLength: 6, nullable: true, example: "17:00" } + created_at: { type: string, format: date-time, readOnly: true } + updated_at: { type: string, format: date-time, readOnly: true } + required: [id, model_name, company_id, recipient] + + SubscriptionWrite: + type: object + description: > + Одно из instance_id / instance_uid / public_instance_id обязательно. + properties: + model_name: { type: string, maxLength: 255 } + instance_id: { type: integer, nullable: true } + instance_uid: { type: string, format: uuid, nullable: true } + public_instance_id: { type: string, format: uuid, nullable: true } + company_id: { type: integer } + service_name: { type: string, maxLength: 255, nullable: true } + recipient: { type: integer, description: ID получателя } + period: + allOf: [{ $ref: '#/components/schemas/PeriodType' }] + nullable: true + weekday: { $ref: '#/components/schemas/WeekdayType' } + first_time: { type: string, maxLength: 6, nullable: true } + second_time: { type: string, maxLength: 6, nullable: true } + required: [model_name, company_id, recipient] + + # ---- Recipient ---- + Recipient: + type: object + properties: + id: { type: integer, readOnly: true } + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128, default: "" } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + email: { type: string, format: email } + user_id: { type: integer, nullable: true } + required: [id] + + RecipientWrite: + type: object + properties: + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128 } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + email: { type: string, format: email } + user_id: { type: integer, nullable: true } + + RecipientUpdate: + type: object + description: "email, user_id, id — только для чтения." + properties: + first_name: { type: string, maxLength: 1024 } + last_name: { type: string, maxLength: 1024 } + phone: { type: string, maxLength: 128 } + telegram_chat_id: { type: string, maxLength: 128, nullable: true } + telegram_username: { type: string, maxLength: 256, nullable: true } + + # ---- NotificationEvent ---- + NotificationEvent: + type: object + properties: + id: { type: integer, readOnly: true } + registered_at: { type: string, format: date-time } + routing_key: { type: string, maxLength: 256, description: "Slug-тег" } + attributes: { type: object, additionalProperties: true, default: {} } + short_description: { type: string, maxLength: 512, nullable: true } + required: [id, registered_at, routing_key] + + NotificationEventWrite: + type: object + properties: + registered_at: { type: string, format: date-time } + routing_key: { type: string, maxLength: 256 } + attributes: { type: object, additionalProperties: true } + short_description: { type: string, maxLength: 512, nullable: true } + required: [registered_at, routing_key] + + # ---- NotificationTemplate ---- + NotificationTemplate: + type: object + properties: + id: { type: integer, readOnly: true } + name: { type: string, maxLength: 1024 } + title: { type: string, maxLength: 1024 } + template_text: { type: string } + attributes: { type: object, additionalProperties: true, default: {} } + event_type: { $ref: '#/components/schemas/EventType' } + model_name: { type: string, maxLength: 255, nullable: true } + required: [id, name, title, template_text] + + NotificationTemplateWrite: + type: object + properties: + name: { type: string, maxLength: 1024 } + title: { type: string, maxLength: 1024 } + template_text: { type: string } + attributes: { type: object, additionalProperties: true } + event_type: { $ref: '#/components/schemas/EventType' } + model_name: { type: string, maxLength: 255, nullable: true } + required: [name, title, template_text] + + # ---- NotificationTransaction ---- + NotificationTransaction: + type: object + properties: + id: { type: integer, readOnly: true } + status: { $ref: '#/components/schemas/TransactionStatus' } + started_at: { type: string, format: date-time } + finished_at: { type: string, format: date-time, nullable: true } + error_message: { type: string, nullable: true } + event: { type: integer, nullable: true, description: "ID связанного события" } + required: [id, status, started_at] + + # ---- Message ---- + Message: + type: object + properties: + id: { type: integer, readOnly: true } + created_at: + type: string + format: date-time + description: > + Объявлено в сериализаторе; в модели Message поле отсутствует (см. замечания). + service_type: { $ref: '#/components/schemas/MessageServiceType' } + transaction: { type: integer, description: "ID транзакции (FK)" } + subject: { type: string, default: "" } + text: { type: string } + recipients: + type: array + items: { type: integer } + description: "ID получателей (M2M recipient.Recipient)" + required: [id, transaction, text] diff --git a/apps/workspaces/.env.example b/apps/workspaces/.env.example index fdb2bb7..0adecee 100644 --- a/apps/workspaces/.env.example +++ b/apps/workspaces/.env.example @@ -1,21 +1,55 @@ -API_PORT=6666 +# ============================================================================ +# workspaces-api — пример переменных окружения +# +# Скопируйте нужные строки в .env в корне репозитория workspaces-api. +# Разбор выполняется библиотекой envconfig (config/config.go, config.FromEnv). +# envconfig НЕ помечает переменные как required — процесс стартует даже без них, +# но без корректных значений БД/documentation сервис работать не будет. +# Пометка (нужна) ниже означает практическую обязательность, а не env-required. +# bool принимает 1/0, true/false, t/f. +# ============================================================================ -API_ADDRESS=0.0.0.0:6666 +# --- HTTP-сервер ------------------------------------------------------------ +API_ADDRESS=0.0.0.0:6666 # (нужна) адрес прослушивания HTTP-сервера host:port (эндпоинт /ping) -POSTGRES_ADDRESS=127.0.0.1 -POSTGRES_USER=user -POSTGRES_PASSWORD=password -POSTGRES_DB=workspaces -POSTGRES_PORT=5432 -POSTGRES_EXTERNAL_PORT=5432 -POSTGRES_POLL_SIZE=10 +# --- PostgreSQL ------------------------------------------------------------- +POSTGRES_ADDRESS=127.0.0.1 # (нужна) хост PostgreSQL +POSTGRES_PORT=5432 # (нужна) порт PostgreSQL +POSTGRES_DB=workspaces # (нужна) имя базы данных +POSTGRES_USER=user # (нужна) пользователь БД +POSTGRES_PASSWORD=password # (нужна) пароль пользователя БД +POSTGRES_POOL_SIZE=10 # размер пула соединений (по умолчанию 0) +ENABLE_SQL_QUERY=1 # логировать SQL-запросы (по умолчанию 0) +ENABLE_SSL=0 # TLS к PostgreSQL с проверкой по YC-PG-CERTIFICATE (по умолчанию 0) +# YC-PG-CERTIFICATE= # содержимое (PEM) CA-сертификата PostgreSQL; нужно при ENABLE_SSL=1 -ENABLE_SQL_QUERY=1 +# --- Сервис documentation --------------------------------------------------- +DOCUMENTATION_HOST=https://stage-api.sarex.io/documentation # (нужна) базовый URL сервиса documentation +DOCUMENTATION_ORIGINATOR=local_ws # идентификатор источника, передаваемый в documentation +DOCUMENTATION_LOGGER_FEATURE=1 # фича логирования обращений к documentation (по умолчанию 0) -DOCUMENTATION_HOST=https://stage-api.sarex.io/documentation -DOCUMENTATION_LOGGER_FEATURE=1 -DOCUMENTATION_ORIGINATOR=local_ws +# --- Bundles ---------------------------------------------------------------- +# BUNDLES_RETRY_COUNT=5 # число ретраев клиента bundles (по умолчанию/при <=0 берётся 3) +# BUNDLES_NJOBS=5 # число параллельных задач при работе с bundles (по умолчанию/при <=0 берётся 3) -SENTRY_DSN=https://62dbe0a9ee5745eead25f2a705cb35ce@o279218.ingest.sentry.io/6229949 -SENTRY_DEBUG=0 -ENVIRONMENT=local \ No newline at end of file +# --- Sentry ----------------------------------------------------------------- +SENTRY_DSN=https://62dbe0a9ee5745eead25f2a705cb35ce@o279218.ingest.sentry.io/6229949 # DSN Sentry +SENTRY_DEBUG=0 # debug-режим Sentry (по умолчанию 0) +ENVIRONMENT=local # имя окружения (передаётся в Sentry как environment) + +# --- Трейсинг OpenTelemetry (необязательно) --------------------------------- +# TRACER_USE=false # включить трейсинг и otel-логгер (по умолчанию false) +# TRACER_HOST=localhost:4317 # адрес OTLP-коллектора +# TRACER_USE_INSECURE=true # подключение без TLS (по умолчанию true) +# SERVICE_NAME=workspaces # имя сервиса в трейсах (по умолчанию workspaces) +# TRACER_LOGGER_NAME=tracer_logger # имя otel-логгера (по умолчанию tracer_logger) + +# --- Вспомогательные (не читаются config.Config) ---------------------------- +POSTGRES_EXTERNAL_PORT=5432 # внешний порт проброса контейнера Postgres в docker-compose +API_PORT=6666 # порт API в docker-compose (сервис api там закомментирован) +# FAKE_API_ADDRESS=0.0.0.0:7777 # адрес фейкового bundle-API (fake_bundle_api/main.go; локальная разработка/тесты) +# NAMESPACE=workspaces # задаётся в манифестах/чарте, кодом приложения не читается +# INTERNAL_PATH=/internal/ # задаётся в чарте, кодом приложения не читается +# DJANGO_HOST=... # задаётся в манифестах iac, но config.Config его НЕ читает +# DJANGO_ORIGINATOR=... # задаётся в манифестах iac, но config.Config его НЕ читает +# DJANGO_BASIC_AUTH=... # инъектируется из секрета в манифестах iac, но config.Config его НЕ читает diff --git a/apps/workspaces/CONFIGURATION.md b/apps/workspaces/CONFIGURATION.md index 6b54bcf..810d00c 100644 --- a/apps/workspaces/CONFIGURATION.md +++ b/apps/workspaces/CONFIGURATION.md @@ -1,122 +1,152 @@ -# Конфигурация проекта workspaces-api +# Конфигурация workspaces-api -Документ описывает все переменные окружения и способы конфигурирования сервиса. +Документ описывает все переменные окружения и способы конфигурирования сервиса `workspaces-api` (репозиторий `pdm/workspaces-api`) и его развёртывания из этого infra-репозитория (`iac/apps/workspaces`). + +`workspaces-api` — HTTP-сервис (`cmd/api`), хранит рабочие пространства (workspaces) и приложения (apps) в PostgreSQL, обращается к сервису documentation и к bundle-сервису. Вместе с ним из одного образа собираются утилиты миграций (`cmd/migrations`) и CLI (`cmd/workspaces-cli`). ## Способы конфигурирования -Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.FromEnv()`). Отдельного конфиг-файла (yaml/toml) у приложения нет. +Сервис настраивается **только через переменные окружения**. Разбор выполняется в `config/config.go` через библиотеку [`envconfig`](https://github.com/kelseyhightower/envconfig) (функция `config.FromEnv()` → `envconfig.Process`). Отдельного конфиг-файла (yaml/toml) у приложения нет. + +В отличие от `env-required`-подхода, **envconfig здесь не помечает переменные обязательными** — при отсутствии значения `FromEnv()` не завершает процесс, поле остаётся нулевым. Поэтому «обязательность» переменных БД/documentation фактическая, а не форсированная кодом: без них сервис стартует, но работать не будет. Источники переменных по способам запуска: | Способ запуска | Откуда берутся переменные | | --- | --- | -| Локально (docker-compose) | Файл `.env` в корне (`env_file: .env`); поднимает только контейнер Postgres, сам API-сервис в compose закомментирован | -| Локально (бинарник) | Переменные окружения процесса; пример значений — в `.env`, приватные — в `.private.env` (см. `.private.env.example`) | -| Kubernetes (Helm) | `.helm/values.yaml`: блок `envs` (обычные значения) и `secretEnvs` (значения из k8s-секретов) | -| CI/CD (GitLab) | `.gitlab-ci.yml`: переменные пайплайна и build-args для сборки образа | +| Локально (docker-compose + бинарник) | Файл `.env` в корне репозитория. `docker-compose.yml` (`env_file: .env`) поднимает только контейнер Postgres (`postgres:13`); сам API-сервис в compose закомментирован и запускается бинарником. `Makefile` (`make docker`) прокидывает `.env` в docker-compose | +| Kubernetes — собственный Helm-чарт репозитория | `.helm/values.yaml` (`universal-chart`): блоки `envs` (обычные значения по окружениям `_default`/`stage`/`preprod`/`production`) и `secretEnvs` (значения из k8s-секретов). Деплой запускается пайплайнами `generic/common-ci` | +| Kubernetes — этот infra-репозиторий (`iac/apps/workspaces`) | `base/` — kustomize-манифесты с инъекцией секретов через **HashiCorp Vault** (annotations `vault.hashicorp.com/*`), обычные переменные заданы инлайн в `env:`. Оверлеи: `yc-k8s-test` (base + postgresql через Flux), `brusnika-stage` и `brusnika-prod` (Flux `HelmRelease` на `universal-chart`, блоки `envs`/`secretEnvs`) | +| CI/CD (GitLab) | `.gitlab-ci.yml` подключает шаблоны `generic/common-ci` (`universal-pipeline.yaml`); в `workflow.rules` задаются переменные пайплайна (`STAND`, `NAMESPACE`, `RELEASE_NAME`, `CHART_NAME`, `CHART_VERSION`, `HELM_SET_ARGS` и т.п.), а также build-args образа | -Порядок запуска в контейнере (`entrypoint.sh`): сначала выполняются миграции (`/migrations migrate`), затем стартует API (`/api`). +**Миграции БД.** Отдельного env-флага для миграций нет: порядок запуска задаёт `entrypoint.sh` — сначала выполняется `/migrations migrate`, затем стартует `/api`. Утилита миграций читает тот же конфиг (`config/config.go`) и использует `ENABLE_SSL`/`YC-PG-CERTIFICATE` для TLS-подключения к БД (`cmd/migrations/main.go`). В kustomize-/Helm-манифестах миграции запускаются той же командой в `args`/`command` контейнера (`set -e; /migrations migrate; exec /api`). + +--- ## Переменные приложения (`config.Config`) -Читаются напрямую по имени. Пустые обязательные значения не приводят к ошибке старта (envconfig не помечает их как required) — но без корректных значений БД/documentation сервис работать не будет. +Читаются структурой `config.Config` (`config/config.go`). Тип `bool` в envconfig принимает `1`/`0`, `true`/`false`, `t`/`f`. -| Переменная | Тип | Значение по умолчанию | Назначение | +### HTTP-сервер + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера (`host:port`), напр. `0.0.0.0:8000`. Эндпоинт `/ping` — liveness/readiness | + +### PostgreSQL + +| Переменная | Тип | По умолчанию | Назначение | | --- | --- | --- | --- | | `POSTGRES_ADDRESS` | string | — | Хост PostgreSQL | | `POSTGRES_PORT` | string | — | Порт PostgreSQL | +| `POSTGRES_DB` | string | — | Имя базы данных | | `POSTGRES_USER` | string | — | Пользователь БД | | `POSTGRES_PASSWORD` | string | — | Пароль пользователя БД | -| `POSTGRES_DB` | string | — | Имя базы данных | | `POSTGRES_POOL_SIZE` | int | `0` | Размер пула соединений к БД | -| `API_ADDRESS` | string | — | Адрес прослушивания HTTP-сервера (`host:port`), напр. `0.0.0.0:8000` | -| `DOCUMENTATION_HOST` | string | — | Базовый URL сервиса документации (documentation service) | -| `DOCUMENTATION_ORIGINATOR` | string | — | Идентификатор источника, передаваемый в сервис документации | -| `DOCUMENTATION_LOGGER_FEATURE` | bool | `false` | Включает фичу логирования обращений к сервису документации | +| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы (query-hook в go-pg) | +| `ENABLE_SSL` | bool | `false` | Подключаться к PostgreSQL по TLS с проверкой по `YC-PG-CERTIFICATE`; читается и в api, и в миграциях | +| `YC-PG-CERTIFICATE` | string | — | Содержимое (PEM) CA-сертификата PostgreSQL; используется при `ENABLE_SSL=1` (`cmd/api/main.go`, `cmd/migrations/main.go`) | + +### Сервис documentation + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `DOCUMENTATION_HOST` | string | — | Базовый URL сервиса documentation | +| `DOCUMENTATION_ORIGINATOR` | string | — | Идентификатор источника, передаваемый в сервис documentation | +| `DOCUMENTATION_LOGGER_FEATURE` | bool | `false` | Включает фичу логирования обращений к сервису documentation | + +### Bundles + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | +| `BUNDLES_RETRY_COUNT` | int | `3` | Кол-во ретраев клиента bundles; при значении `<= 0` в `FromEnv()` принудительно берётся `3` | +| `BUNDLES_NJOBS` | int | `3` | Кол-во параллельных задач при работе с bundles; при значении `<= 0` берётся `3` | + +### Sentry + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | | `SENTRY_DSN` | string | — | DSN для отправки ошибок в Sentry | | `SENTRY_DEBUG` | bool | `false` | Debug-режим Sentry | | `ENVIRONMENT` | string | — | Имя окружения (передаётся в Sentry как environment) | -| `BUNDLES_RETRY_COUNT` | int | `3` | Кол-во ретраев клиента bundles (если задано `<= 0`, берётся `3`) | -| `BUNDLES_NJOBS` | int | `3` | Кол-во параллельных задач при работе с bundles (если `<= 0`, берётся `3`) | -| `ENABLE_SQL_QUERY` | bool | `false` | Логировать SQL-запросы (добавляет query-hook в go-pg) | -| `YC-PG-CERTIFICATE` | string | — | Содержимое (PEM) CA-сертификата PostgreSQL; используется при `ENABLE_SSL=1` | -| `ENABLE_SSL` | bool | `false` | Подключаться к PostgreSQL по TLS с проверкой по `YC-PG-CERTIFICATE` | + +### Трейсинг (OpenTelemetry) + +| Переменная | Тип | По умолчанию | Назначение | +| --- | --- | --- | --- | | `TRACER_USE` | bool | `false` | Включает OpenTelemetry-трейсинг и otel-логгер | | `TRACER_HOST` | string | `localhost:4317` | Адрес OTLP-коллектора | -| `SERVICE_NAME` | string | `workspaces` | Имя сервиса в трейсах | | `TRACER_USE_INSECURE` | bool | `true` | Небезопасное (без TLS) подключение к коллектору | +| `SERVICE_NAME` | string | `workspaces` | Имя сервиса в трейсах | | `TRACER_LOGGER_NAME` | string | `tracer_logger` | Имя otel-логгера | -> Тип `bool` в envconfig принимает `1`/`0`, `true`/`false`, `t`/`f` и т.п. +> Дефолты `TRACER_*` и `SERVICE_NAME` заданы прямо в тегах `default:"..."` структуры `config.Config`; остальные поля дефолтов не имеют (нулевое значение типа). -## Переменные инфраструктуры, сборки и вспомогательных утилит +--- -Не читаются основным кодом приложения, но участвуют в запуске/сборке/деплое. +## Инфраструктурные, сборочные и вспомогательные переменные + +Не читаются основным кодом приложения (`config.Config`), но участвуют в запуске/сборке/деплое. | Переменная | Где используется | Назначение | | --- | --- | --- | -| `POSTGRES_EXTERNAL_PORT` | `docker-compose.yml` | Внешний порт проброса контейнера Postgres | -| `API_PORT` | `.env`, `docker-compose.yml` (закомментированный сервис) | Порт API при локальном запуске в контейнере | -| `FAKE_API_ADDRESS` | `fake_bundle_api/main.go` | Адрес фейкового bundle-API (только для локальной разработки/тестов) | +| `POSTGRES_EXTERNAL_PORT` | `.env`, `docker-compose.yml` | Внешний порт проброса контейнера Postgres | +| `API_PORT` | `.env`, `docker-compose.yml` (закомментированный сервис api) | Порт API при локальном запуске в контейнере | +| `FAKE_API_ADDRESS` | `fake_bundle_api/main.go` (`envconfig`) | Адрес фейкового bundle-API для локальной разработки/тестов; читается отдельной утилитой, не основным сервисом | | `GITLAB_CREDENTIALS` | `api.Dockerfile` (build-arg) | Креды `https://:@gitlab.sarex.io` для доступа к приватным Go-модулям при сборке | -| `CI_COMMIT_SHORT_SHA` | `api.Dockerfile` (build-arg) → `APP_VERSION` | Версия/хэш коммита, зашиваемая в бинарь при сборке (`make api`) | -| `APP_VERSION` | `Makefile` (`make api`) | Версия приложения при сборке | +| `CI_COMMIT_SHORT_SHA` | `api.Dockerfile` (build-arg) → `APP_VERSION` | Хэш коммита, прокидываемый в сборку `make api` | +| `APP_VERSION` | `Makefile` (`make api`) | Версия приложения при сборке (значение — из `CI_COMMIT_SHORT_SHA`) | +| `NAMESPACE` | `.helm/values.yaml`, `base/*.yaml`, brusnika-оверлеи | Задаётся в манифестах, но **кодом приложения не читается** | +| `INTERNAL_PATH` | `.helm/values.yaml` | Задаётся в чарте, но **кодом приложения не читается** | +| `DJANGO_HOST`, `DJANGO_ORIGINATOR` | `base/backend-deployment.yaml`, brusnika-оверлеи | Заданы в манифестах, но **`config.Config` их не читает** (в текущем коде полей Django нет) | +| `DJANGO_BASIC_AUTH` | `base/backend-deployment.yaml` (Vault), brusnika `secretEnvs` | Инъектируется из секрета, но **`config.Config` его не читает** | -## Переменные из Helm-чарта (`.helm/values.yaml`) +--- -Обычные переменные (`envs`) — переопределяют дефолты по окружениям (`_default`/`stage`/`preprod`/`production`): +## Деплой из этого репозитория (`iac/apps/workspaces`) -| Переменная | Значение `_default` | Примечание | -| --- | --- | --- | -| `POSTGRES_POOL_SIZE` | `3` | | -| `BUNDLES_RETRY_COUNT` | `5` | | -| `BUNDLES_NJOBS` | `5` | | -| `API_ADDRESS` | `0.0.0.0:8000` | В preprod/production — порт `8080` | -| `NAMESPACE` | `workspaces` | Задаётся в чарте, но **кодом приложения не читается** | -| `ENABLE_SQL_QUERY` | `0` | | -| `ENABLE_SSL` | `1` | | -| `DOCUMENTATION_HOST` | `http://documentations-api-svc.documentations:8000` | Зависит от окружения | -| `DOCUMENTATION_LOGGER_FEATURE` | `0` | | -| `DOCUMENTATION_ORIGINATOR` | `stage_ws` | В prod — `prod_ws` | -| `SENTRY_DSN` | `https://…@o279218.ingest.sentry.io/6229949` | | -| `SENTRY_DEBUG` | `0` | | -| `ENVIRONMENT` | `stage` | В prod — `prod` | -| `TRACER_USE` | `1` | | -| `TRACER_HOST` | `signoz-otel-collector.signoz.svc.cluster.local:4317` | Зависит от окружения | -| `SERVICE_NAME` | `workspaces-api.platform` | Зависит от окружения | -| `TRACER_USE_INSECURE` | `1` | | -| `INTERNAL_PATH` | `/internal/` | Задаётся в чарте, но **кодом приложения не читается** | +Здесь используется **kustomize** (`base/` + оверлеи), а не собственный Helm-чарт сервиса. Секреты БД и Django инъектируются агентом **Vault** и подгружаются в окружение процесса до старта (`set -a; . /vault/secrets/...; exec /api`). -Переменные из секретов (`secretEnvs`, секрет `workspaces-postgresql-secret` / `ya-pg-secret`): +### `base/` -| Переменная | Ключ секрета | -| --- | --- | -| `POSTGRES_USER` | `username` | -| `POSTGRES_PASSWORD` | `password` | -| `YC-PG-CERTIFICATE` | `ca.crt` | -| `POSTGRES_ADDRESS` | `host` | -| `POSTGRES_PORT` | `port` | -| `POSTGRES_DB` | `database` | +- `backend-deployment.yaml` (api, namespace `workspaces`) — обычные переменные заданы инлайн в `env:` (`POSTGRES_POOL_SIZE`, `BUNDLES_*`, `API_ADDRESS`, `NAMESPACE`, `ENABLE_SQL_QUERY`, `ENABLE_SSL`, `DOCUMENTATION_*`, `ENVIRONMENT`, `DJANGO_HOST`, `DJANGO_ORIGINATOR`). Vault-шаблоны формируют файл `/vault/secrets/workspaces-db` с `POSTGRES_ADDRESS/PORT/DB/USER/PASSWORD` (из `secrets/data/postgresql/apps/workspaces`) и `/vault/secrets/workspaces-django-auth` с `DJANGO_BASIC_AUTH` (из `secrets/data/vault/common/django_auth`). Контейнер стартует через `command: /bin/sh -ec` + `args`, который подгружает эти файлы (`set -a; . /vault/secrets/...`) и `exec /api`. Vault-роль — `workspaces`, ServiceAccount — `workspaces-vault`. Namespace размечен `istio-injection: enabled`. +- `frontend-deployment.yaml` (`frontend`, образ `workspaces-v2-frontend`) и `frontend-service.yaml` — статический фронтенд, переменных окружения не имеет. +- `backend-service.yaml` (`backend-svc`, `:80 → 8000`), `namespace.yaml`, `serviceaccount.yaml`. +- `kustomization.yaml` собирает namespace, serviceaccount, backend/frontend deployments и services (namespace `workspaces`). + +### Оверлеи + +- **`yc-k8s-test`** — `../base` + `postgresql.yaml` (Flux `HelmRelease` `postgresql-contour`: БД `workspaces_db`, пользователь `workspaces`, расширение `uuid-ossp`, восстановление из дампа, интеграция с Vault). Патч `replicas.yaml` (`replicas: 1` для `workspaces-api`). +- **`brusnika-stage`** и **`brusnika-prod`** — Flux `HelmRelease` на `universal-chart` (`0.1.7`) для api и фронтенда. Переменные — в `envs`, секреты — в `secretEnvs` (`postgres-secret` → `POSTGRES_USER`/`POSTGRES_PASSWORD`, `django-auth` → `DJANGO_BASIC_AUTH`). Отличаются значениями `POSTGRES_ADDRESS` и `DOCUMENTATION_HOST` (stage: `192.168.2.45` / `https://test.sarex.brusnika.tech/documentations`; prod: `postgres-service` / `https://cde.brusnika.ru/documentations`). Api-под запускает `/migrations migrate` перед `/api` в `args`. + +--- ## JWT-ключи -RSA-ключи для JWT лежат в `.pub_keys/` и при сборке образа копируются в `/etc/sarex/keys` (см. `api.Dockerfile`). Пути к ключам передаются в функции работы с токенами (`pkg/gotools/auth/jwt.go`) как аргументы, отдельной переменной окружения для пути к ключам в текущем коде нет. +RSA-ключи для JWT лежат в `.pub_keys/` (`prod.rsa.pub`, `stage.rsa.pub`, `test.rsa*`) и при сборке образа копируются в `/etc/sarex/keys` (см. `api.Dockerfile`). Пути к ключам передаются в код работы с токенами как аргументы; отдельной переменной окружения для пути к ключам в текущем коде нет. + +--- ## Замечания и потенциальные проблемы -- В файле `.env` переменная названа `POSTGRES_POLL_SIZE` (опечатка), тогда как код читает `POSTGRES_POOL_SIZE`. При локальном запуске из `.env` размер пула не применится и останется `0`. +- **Опечатка в `.env`:** переменная названа `POSTGRES_POLL_SIZE`, тогда как код читает `POSTGRES_POOL_SIZE`. При локальном запуске из `.env` размер пула не применится и останется `0`. +- **`env-required` не используется:** отсутствие обязательных значений (БД, documentation) не приводит к ошибке `FromEnv()` — процесс стартует с пустыми полями и падает позже при обращении к БД/сервисам. +- **Django-переменные не читаются кодом:** `DJANGO_HOST`, `DJANGO_ORIGINATOR`, `DJANGO_BASIC_AUTH` заданы в манифестах `iac` (base + brusnika-оверлеи), но в `config.Config` соответствующих полей нет — значения игнорируются приложением. +- **`NAMESPACE` и `INTERNAL_PATH`** задаются в манифестах/чарте, но кодом приложения не читаются. - Имя переменной `YC-PG-CERTIFICATE` содержит дефисы — envconfig сопоставляет её по точному совпадению тега. -- В `.helm/values.yaml` у `secretEnvs.POSTGRES_PORT` значение `_default.secretName` указано как `a-pg-secret` (похоже на опечатку, ожидается `ya-pg-secret`/`workspaces-postgresql-secret`). -- Переменные `NAMESPACE` и `INTERNAL_PATH` задаются в Helm-чарте, но не используются в коде приложения. +- **Расхождение секретов между источниками деплоя:** в `.helm/values.yaml` `secretEnvs.POSTGRES_PORT._default.secretName` указан как `a-pg-secret` (похоже на опечатку от `ya-pg-secret`); ключи секрета БД различаются между окружениями (`workspaces-postgresql-secret` с ключами `username`/`ca.crt` vs `ya-pg-secret`/`yc-pg-certificate`). В kustomize (`base/`) те же значения приходят из Vault, а не из k8s-секретов. +- **Порты различаются по окружениям:** локально/`_default` — `8000`, в preprod/production Helm-чарта — `8080` (см. `API_ADDRESS` и probes). + +--- ## Минимальный набор для локального запуска -Для запуска сервиса локально (Postgres — через docker-compose, API — бинарником) нужно задать: +Postgres — через docker-compose, api — бинарником (миграции применяются `entrypoint.sh`/вручную перед стартом): -- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` - `API_ADDRESS` (напр. `0.0.0.0:6666`) +- `POSTGRES_ADDRESS`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` - `DOCUMENTATION_HOST`, `DOCUMENTATION_ORIGINATOR` - `ENVIRONMENT` (напр. `local`) -- при необходимости — `SENTRY_DSN`, `ENABLE_SQL_QUERY` +- при необходимости — `ENABLE_SQL_QUERY`, `SENTRY_DSN`, `ENABLE_SSL` (+ `YC-PG-CERTIFICATE`), `TRACER_*` -См. пример значений в `.env`. +См. пример значений в `.env` / `.env.example`.