- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
192 lines
16 KiB
Markdown
192 lines
16 KiB
Markdown
# Конфигурация
|
||
|
||
Конвенция: *как* устроена и грузится конфигурация transcriber (TOML).
|
||
Правила оформления кода (How), не спецификация поведения.
|
||
|
||
**Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту.
|
||
Главные: комментариями снабжена половина полей; единого места проверки на старте
|
||
нет: у секций `[auth]`, `[pipeline]` и `[storage]` свой `Validate()` в точке
|
||
входа, а пустые ключи `[yandex]` ловит конструктор распознавателя.
|
||
|
||
**Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml`
|
||
([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки
|
||
приезжают из TOML»; `godotenv` в `cmd/transcriber` по-прежнему загружает `.env`, но кладёт
|
||
его в окружение процесса, а не в настройки приложения.
|
||
|
||
## Принципы
|
||
|
||
- **Конфигурация — только TOML.** Переменные окружения для конфигурации **не
|
||
используем**: окружение наследуется дочерними процессами и видно через
|
||
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
|
||
*Расхождение:* `cmd/transcriber` зовёт `godotenv.Load()` и молча продолжает без файла.
|
||
- Грузим **один раз при старте** в одну типизированную структуру `Config`
|
||
(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в
|
||
прикладном коде нет, только загрузчик `internal/config`.
|
||
- Конфиг **неизменяем** после старта; смена параметров — перезапуск процесса.
|
||
|
||
## Файл и поиск
|
||
|
||
- Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочем каталоге**
|
||
процесса.
|
||
- Путь переопределяется опцией **`-c path`** или **`--config=path`**.
|
||
- Образец в репозитории — **`config.example.toml`** (см. ниже); реальный
|
||
`config.toml` не коммитится.
|
||
|
||
## config.example.toml — самодокументируемый образец
|
||
|
||
`config.example.toml` коммитим как единый справочник по конфигу: все секции и
|
||
все поля. **Каждое поле снабжаем комментарием**, из которого ясно:
|
||
|
||
- **зачем** поле — что оно меняет в поведении;
|
||
- **допустимые значения** — перечисление или границы;
|
||
- **единицы измерения**, если применимы — секунды, байты, доля `0–1`.
|
||
|
||
```toml
|
||
[server]
|
||
port = <N> # порт HTTP-сервера
|
||
shutdown_timeout = <N> # ждать мягкой остановки сервера, секунды
|
||
force_shutdown_timeout = <N> # ждать остановки воркеров, секунды
|
||
```
|
||
|
||
Значения намеренно заменены плейсхолдерами: предмет конвенции — форма
|
||
комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и
|
||
начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут
|
||
одним домом — таблица «Настройки с числовым значением» в
|
||
[../database.md](../database.md); `config.example.toml` — источник истины по
|
||
составу полей.
|
||
|
||
Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»).
|
||
|
||
*Расхождение:* перечень доверенных адресов в секции `[auth]` образца заполнен
|
||
примером — подсетью docker, — а не оставлен пустым: пустое значение не говорит,
|
||
какой формы значение здесь ждут, а сервис с пустым перечнем не поднимается вовсе.
|
||
Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным
|
||
входом.
|
||
|
||
Там же, комментарием под секцией, стоит **рецепт локального входа одним связным
|
||
блоком**: три правки сверху вниз — пара петлевых адресов в перечень доверенных,
|
||
`[server] debug = true`, раскомментированная секция `[auth.test_headers]` с
|
||
ключом `Remote-User`. Блок один, а не три комментария по месту: правки связаны
|
||
между собой, и применённая порознь любая из них роняет старт либо оставляет
|
||
сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение —
|
||
перечень с адресом прокси и `debug = false`, — а секция имитации закомментирована
|
||
целиком: образец описывает боевую выкладку, а локальный вход — способ до неё
|
||
дойти, и два рабочих значения в одном файле читались бы как выбор без указания,
|
||
какое из них чьё.
|
||
|
||
*Расхождение:* петлевые адреса в рецепте названы **парой** — `127.0.0.1` и
|
||
`::1`, — а не одним значением, хотя правило секции требует от образца только
|
||
формы значения. Причина в цене: браузер разрешает `localhost` в IPv6 не реже,
|
||
чем в IPv4, и перечень без `::1` даёт неузнанный запрос там, где человек ждёт
|
||
входа.
|
||
|
||
## Поля по дискриминатору `type`
|
||
|
||
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из
|
||
бекендов или внешних сервисов), обязательность и опциональность полей определяет
|
||
значение `type`, а не фиксированный список секции.
|
||
|
||
- **Проверка — по `type`.** Для каждого поддерживаемого значения свой набор
|
||
обязательных полей; поля других значений не требуются. Неизвестное значение —
|
||
ошибка на старте с перечислением поддерживаемых.
|
||
- **Образец — по `type`.** В `config.example.toml`:
|
||
- основное (умолчательное) значение **предзаполнено** рабочими значениями;
|
||
- альтернативные — **блоками-комментариями ниже**, каждый со своим описанием
|
||
полей (зачем, границы, единицы — как у обычных полей);
|
||
- так из примера видны все варианты и поля каждого, не открывая код.
|
||
|
||
Дискриминатора в transcriber пока нет; правило записано на случай второго
|
||
распознавателя.
|
||
|
||
## Секреты
|
||
|
||
Секреты доставляет **выкладка**, рендеря их прямо в `config.toml` (transcriber:
|
||
Ansible из `pet-project-server`). Приложение просто читает TOML — отдельного слоя
|
||
секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не
|
||
репозиторий и не окружение.
|
||
|
||
- Секретные поля transcriber: `yandex.speech_kit_api_key`,
|
||
`yandex.object_storage_access_key_id`,
|
||
`yandex.object_storage_secret_access_key`. Секрет клиента OIDC отсюда ушёл
|
||
2026-08-22 вместе с собственным входом.
|
||
- Отрендеренный `config.toml` (с секретами) **не коммитится**; права `0600`,
|
||
владелец — пользователь процесса (`1000:1000`).
|
||
- В `config.example.toml` секретные поля — пустые строки.
|
||
*Расхождение:* сейчас там стоят подсказки вида `your_..._here`, а не пустые
|
||
строки, и загрузчик их не отличает от настоящего значения.
|
||
- Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво
|
||
отрендеренный файл) — см. «Проверка и остановка на старте».
|
||
- В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность».
|
||
- **Отказ загрузки настроек не несёт содержимого файла.** Текст такого отказа
|
||
собирает библиотека разбора, и собирает она его из разбираемого куска:
|
||
`toml.ParseError` кладёт в сообщение само значение («Invalid float value: %q»).
|
||
Оборванная кавычка в строке секретного ключа — типовая поломка криво
|
||
отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а
|
||
инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ
|
||
разбора пересобирается своими словами: путь, строка, столбец и последний ключ,
|
||
без сообщения библиотеки. Прочие отказы декодера (несовпадение типов,
|
||
неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их
|
||
текст остаётся как есть — иначе за разборчивость отказа платили бы там, где
|
||
платить не за что.
|
||
|
||
## Проверка и остановка на старте
|
||
|
||
Конфиг проверяем **на старте, до приёма трафика**. Негодный конфиг — лог `ERROR`
|
||
и выход с ненулевым кодом, не стартуем наполовину.
|
||
|
||
Что проверяем:
|
||
|
||
- обязательные поля заданы;
|
||
- каталоги хранилища существуют и доступны на запись;
|
||
- границы числовых полей соблюдены;
|
||
- ключи внешних сервисов не пусты.
|
||
|
||
*Расхождение:* `LoadConfig` проверяет только существование файла и разбирает
|
||
TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс
|
||
выходит с кодом 1. Единого места проверки нет.
|
||
|
||
Два ключа секции `[telegram]`, стоявшие здесь исключением, ушли вместе с самим
|
||
входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.
|
||
|
||
**Проверка, охватывающая две секции разом, живёт методом на корневой `Config`.**
|
||
Такая сегодня одна — `ValidateTestHeaders`: она судит `[server] debug` против
|
||
`[auth.test_headers]`, и ни в `Validate()` секции сервера, ни в `Validate()`
|
||
секции входа не помещается — секция начала бы знать о чужой секции. Зовётся она
|
||
из `cmd/transcriber` рядом с остальными. Имена принимаемых заголовков приходят
|
||
ей **доводом**, а не читаются из пакета настроек: дом у них один — константы
|
||
транспорта, — а `internal/config` транспорта не знает и знать не должен, иначе
|
||
`cmd/devtools`, которому нужен один разбор конфига, линковал бы всю поверхность
|
||
HTTP.
|
||
|
||
Секции `[auth]`, `[pipeline]` и `[storage]` проверяют себя сами, и проверка стоит
|
||
на старте: `Validate()` каждой зовётся из `cmd/transcriber` сразу после загрузки
|
||
и роняет процесс с именем незаполненного ключа. У `[storage]` это ожидание занятой
|
||
базы и число соединений читающего пула: ноль у первого отдаёт «база занята»
|
||
первому же воркеру, ноль у второго означает пул без предела — то есть настройку,
|
||
которой не управляют. Причина в цене умолчания: поднявшись с
|
||
пустым перечнем доверенных адресов, сервис не узнавал бы никого, а узнать об
|
||
этом было бы неоткуда — все адреса приложения просто отвечали бы отказом.
|
||
Сообщение называет **имя ключа**; правило «значения в отказ не идут» остаётся в
|
||
силе для прочих секций, где секреты есть.
|
||
|
||
## Структура в коде
|
||
|
||
- Весь разбор и проверка — в `internal/config`; наружу отдаётся готовая `Config`.
|
||
- Одна корневая структура `Config` с под-структурами по секциям. Перечень
|
||
секций и полей здесь не повторяем: источник истины по составу —
|
||
`config.example.toml`, действующие числа — [../database.md](../database.md),
|
||
«Настройки с числовым значением». Каталог данных задаётся одним ключом
|
||
`[storage] data_dir`
|
||
([ADR](../adr/ADR-2026-08-12-single-data-dir-config-key.md)).
|
||
- Умолчания задаются в `defaultConfig()`, файл их перекрывает. Новое поле
|
||
требует правки обоих мест.
|
||
- **Обязательное поле — поле, у которого умолчания нет намеренно.** Умолчание у
|
||
такого поля было бы угаданным намерением, и одна из двух ошибок стала бы
|
||
тихой. Форма записи: умолчания нет ни в `defaultConfig()` (причина — строкой
|
||
комментария у самого поля), ни по нулевому значению типа; присутствие ключа
|
||
судит **разбор** — `MetaData.IsDefined` из `toml.DecodeFile`, — потому что
|
||
значение отличить «не задано» от «задано нулём» не позволяет. В
|
||
`config.example.toml` у поля стоит значение свежей установки. Первым таким
|
||
полем был `telegram.enabled`; секция убрана 2026-08-14, и живого примера у
|
||
правила сейчас нет.
|