Files
chatballs/apps/backend/chatballs_backend/settings_database.py
T
Andrey 41a4b0dd84 ✨ feat: запуск без .env — всё настраивается в UI, и порталы по дизайну
Первичное требование: поднял докер — прошёл мастер — дальше всё в интерфейсе.
Ни одной переменной окружения задавать не нужно и негде.

Запуск и секреты:
- .env.example удалён, мёртвые переменные вычищены; секреты инстанса
  генерирует одноразовый сервис secrets в именованный том, пароли БД —
  через POSTGRES_*_PASSWORD_FILE;
- Caddy: catch-all :80 и on_demand TLS вместо хостов в конфиге — свежая
  коробка отвечает по IP и по любому домену, до мастера дойти можно;
- адрес установки, SMTP и TURN переехали в настройки (InstanceSettings,
  миграции 0027–0030), внешние ссылки строятся от него;
- deploy/cli больше не читает .env; сборка образов в ghcr через GitHub
  Actions, release.env с digest-пинами;
- куки Secure/__Host- выставляются по факту TLS запроса, а не настройкой.

Порталы (дизайн-базлайн v2, кадры PT1–PT8):
- список, карточка портала, библиотека материалов, редактор статьи и
  настройки — ширины и ритм как в остальных разделах;
- файлы статьи: изображение вставляется в текст своим механизмом, файл
  прикрепляется вложением и выводится на портале списком с иконкой формата;
- ссылки на файлы приводятся к относительным: абсолютный хост резал CSP
  портала и картинка не появлялась;
- «Опубликовать» сверяется с сервером и публикует то, что на экране, а не
  ранее выбранную редакцию;
- колонка «Оценки» в списке статей и блок оценок в редакторе.

Первый запуск: полоса «демо-данные устанавливаются» — установка идёт в
worker, и без неё человек видел пустые разделы без объяснения.

Починен фон: вторичное хранилище стало best-effort — при живых остаточных
S3-ключах повторная загрузка файла падала в 500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:37:48 +03:00

94 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from django.core.exceptions import ImproperlyConfigured
from chatballs_backend.settings_env import env_secret
def _credentials() -> tuple[dict[str, str], dict[str, str]]:
# Имена ролей — константы продукта, а не настройка установки: их создаёт
# deploy/postgres/init-runtime-roles.sh при первом старте. Переменные
# окружения остаются переопределением для нестандартных установок.
users = {
"app": os.environ.get("POSTGRES_APP_USER", "chatballs_app"),
"platform": os.environ.get("POSTGRES_PLATFORM_USER", "chatballs_platform"),
"migration": os.environ.get(
"POSTGRES_MIGRATION_USER", "chatballs_migration"
),
}
# Пароли ролей генерирует первый старт стека в том с секретами; человек их
# не вводит и не хранит. Переменные окружения остаются переопределением.
fallback = env_secret("POSTGRES_PASSWORD", "postgres_password", "chatballs")
passwords = {
"app": env_secret("POSTGRES_APP_PASSWORD", "postgres_app_password", fallback),
"platform": env_secret(
"POSTGRES_PLATFORM_PASSWORD", "postgres_platform_password", fallback
),
"migration": env_secret(
"POSTGRES_MIGRATION_PASSWORD", "postgres_migration_password", fallback
),
}
return users, passwords
def _pool_options(*, testing: bool) -> dict | None:
# ASGI-серверы исполняют ORM в короткоживущих потоках sync_to_async;
# persistent-соединения (CONN_MAX_AGE > 0) в таких потоках осиротевают и
# исчерпывают max_connections Postgres. Вместо них — psycopg pool на процесс:
# соединения возвращаются в пул независимо от потока и ограничены сверху.
# CHATBALLS_DB_POOL_MAX=0 отключает пул (короткоживущие соединения на запрос).
if testing:
return None
max_size = int(os.environ.get("CHATBALLS_DB_POOL_MAX", "4"))
if max_size <= 0:
return None
return {
"min_size": int(os.environ.get("CHATBALLS_DB_POOL_MIN", "1")),
"max_size": max_size,
"timeout": float(os.environ.get("CHATBALLS_DB_POOL_TIMEOUT", "10")),
}
def build_databases(*, debug: bool, testing: bool) -> dict[str, dict]:
role = os.environ.get("CHATBALLS_DB_ROLE", "app").lower()
if role not in {"app", "platform", "migration"}:
raise ImproperlyConfigured("CHATBALLS_DB_ROLE must be app, platform or migration")
users, passwords = _credentials()
if not debug and not testing and len(set(users.values())) != 3:
raise ImproperlyConfigured(
"App, platform and migration database users must be distinct"
)
pool = _pool_options(testing=testing)
def config(selected_role: str) -> dict:
return {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB", "chatballs"),
"USER": users[selected_role],
"PASSWORD": passwords[selected_role],
"HOST": os.environ.get("POSTGRES_HOST", "postgres"),
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
# Пул несовместим с persistent-соединениями: с ним CONN_MAX_AGE
# обязан быть 0, а без пула persistent-режим возвращать нельзя
# (см. _pool_options).
"CONN_MAX_AGE": 0,
"OPTIONS": {"pool": dict(pool)} if pool else {},
}
databases = {
"default": config("migration" if testing else role),
"platform": config("platform"),
}
if testing:
# Тесты создают свою БД и подключаются владельцем кластера. Его пароль
# приходит оттуда же, откуда у остальных ролей: файл секрета инстанса,
# переменная окружения — переопределение.
databases["default"]["USER"] = os.environ.get(
"POSTGRES_USER", "chatballs_bootstrap"
)
databases["default"]["PASSWORD"] = env_secret(
"POSTGRES_PASSWORD", "postgres_password", "chatballs"
)
databases["platform"]["TEST"] = {"MIRROR": "default"}
return databases