Схема и миграции
Схема Postgres — это контракт между двумя сервисами на разных языках. Панель ей владеет и мигрирует, гейт её только читает.
Одно место, где форма определена: migrations/ в репозитории панели.
app/models.py — отображение этой схемы, а не её источник; в нём ничего не
создаёт таблиц. Go-структуры в internal/store/ — тоже отображение.
Как менять¶
cd zbgate-admin
# новая ревизия: следующий номер, down_revision — текущая голова
$EDITOR migrations/versions/00NN_что_меняем.py
.venv/bin/alembic upgrade head && .venv/bin/alembic downgrade base
Прогон вниз до нуля — обязательная часть проверки, а не формальность: миграция, которая не откатывается, это миграция, которую нельзя выкатить в пятницу.
Дальше — БД-тесты гейта против новой схемы. Они проверяют расшифровку секретов, авторизацию, лимиты, доступы, запись логов и горячий релоад, то есть ровно те места, где схема и код должны совпадать. См. Разработка и тесты.
Порядок выката¶
Миграция бывает двух видов, и разница существенна.
Совместимая — добавляет колонку или таблицу. Работающий гейт старой версии её не замечает. Выкатывается когда угодно.
Несовместимая — убирает или переименовывает то, что гейт читает. Гейт старой версии после неё сломается: колонки, которую он выбирает, больше нет. Такие миграции помечены в шапке файла, и панель с гейтом выкатываются вместе.
Пример из истории: миграция, перенёсшая наценку с ключа на учётную запись,
удалила user_keys.cost_multiplier. Гейт до этой версии перестал бы находить
ключи вовсе — не «терял бы наценку», а отвечал бы 401 на всё.
Уведомления об изменении¶
Гейт держит конфиг в памяти и узнаёт о правках по LISTEN/NOTIFY на канале
zbgate_config. Триггер стоит на каждой таблице конфигурации:
providers, provider_models, provider_keys, user_keys, model_prices,
model_price_windows, model_price_tiers, gateway_settings, admin_users.
Новая таблица конфигурации должна получить такой же триггер — иначе правка в
ней подействует только после истечения кэша или перезапуска. admin_users в
этом списке именно поэтому: наценка переехала туда, и без триггера её изменение
ждало бы минуту.
CREATE TRIGGER имя_таблицы_notify
AFTER INSERT OR UPDATE OR DELETE ON имя_таблицы
FOR EACH STATEMENT EXECUTE FUNCTION zbgate_notify_config();
Отдельно есть zbgate_touch_updated_at() — updated_at поддерживает база, а не
писатель, чтобы гейт, обновляющий last_used_at, или ручная правка SQL не
оставляли его устаревшим.
Секреты¶
ZBGATE_SECRET_KEY — один 32-байтный ключ AES-256-GCM на оба сервиса. Панель
шифрует секреты провайдеров, гейт расшифровывает.
Формат хранения nonce(12) || ciphertext || tag(16) — одинаковый в Go и Python
без всякого обрамления, потому что и cipher.AEAD.Seal, и AESGCM.encrypt
дописывают тег к шифротексту сами.
- Дамп базы без ключа секретов не выдаёт.
- Потеря ключа делает все сохранённые секреты нечитаемыми: ротация означает перевод всех ключей провайдеров заново.
- Расшифровка, упавшая на неверном теге, почти всегда означает подменённый ключ, а не битую строку — так и написано в сообщении, вместо голой ошибки крипто.
Ключи пользователей — SHA-256, только дайджест. Утёкшая база не воспроизводится против гейта. Пароли администраторов — Argon2id.
Расширения Postgres¶
Никаких. Вся криптография — в коде приложения: AES-256-GCM и SHA-256 в Python, то же самое в Go.
Раньше первая миграция создавала pgcrypto, которым ничего не пользовалось.
Это требовало прав суперюзера при установке — чего управляемый Postgres может и
не дать — и падало на минимальных сборках. Убрано.
Таблицы¶
| Таблица | Что в ней |
|---|---|
providers |
апстримы: тип, адрес, флаги диалекта, нагрузка, повторы |
provider_models |
таблица маршрутов; model уникален глобально |
provider_keys |
секреты провайдеров, вес, ограничение по моделям |
user_keys |
ключи клиентов: хеш, лимиты, бюджет, доступы, владелец |
admin_users |
учётные записи: роль, наценка, потолок ключей, метки |
model_prices |
ставки за токены и за минуту аудио |
model_price_tiers |
ступени по длине контекста |
model_price_windows |
окна по часам |
gateway_settings |
одна строка: логирование, тарификация, самообслуживание |
ldap_settings |
одна строка: каталог |
request_logs |
журнал |
request_log_tools |
вызовы инструментов, по строке на вызов |
gateway_settings, ldap_settings — синглтоны, охраняемые CHECK (id = 1).