Add example .env files and configuration documentation for measurements and subscriptions services.

This commit is contained in:
emelinda 2026-07-13 20:35:36 +03:00
parent 1b5f5a1f67
commit 5f7fe11748
9 changed files with 2823 additions and 84 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

View File

@ -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` рядом с этим файлом.

View File

@ -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]

View File

@ -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
# --- 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 его НЕ читает

View File

@ -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://<user>:<token>@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`.