- имя приведено к конвенции, которая сама называла его расхождением; ссылки поправлены в README, CLAUDE.md, конвенциях, docs/review.md и двух записях задач - в шапке docs/conventions/config.md заодно поправлен второй пункт перечня: проверки на старте у `[auth]` и `[telegram]` уже есть - архив openspec/changes/archive/ не тронут: это запись о прошлом
14 KiB
Конфигурация
Конвенция: как устроена и грузится конфигурация transcriber (TOML). Правила оформления кода (How), не спецификация поведения.
Взято из проекта jellybit. Расхождения с сегодняшним кодом названы по месту.
Главные: комментариями снабжена половина полей; единого места проверки на старте
нет: у секций [auth] и [telegram] свой Validate() в main.go, а пустые
ключи [yandex] ловит конструктор распознавателя.
Механизировано: запрет os.Getenv — forbidigo в .golangci.yml
(go-linters.md, «Механизировано»). Он держит правило «настройки
приезжают из TOML»; godotenv в main.go по-прежнему загружает .env, но кладёт
его в окружение процесса, а не в настройки приложения.
Принципы
- Конфигурация — только TOML. Переменные окружения для конфигурации не
используем: окружение наследуется дочерними процессами и видно через
/proc/<pid>/environ— для секретов это слабее файла под0600. Расхождение:main.goзовётgodotenv.Load()и молча продолжает без файла. - Грузим один раз при старте в одну типизированную структуру
Config(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в прикладном коде нет, только загрузчикinternal/config. - Конфиг неизменяем после старта; смена параметров — перезапуск процесса.
Файл и поиск
- Имя конфига по умолчанию —
config.toml, ищется в рабочем каталоге процесса. - Путь переопределяется опцией
-c pathили--config=path. - Образец в репозитории —
config.example.toml(см. ниже); реальныйconfig.tomlне коммитится.
config.example.toml — самодокументируемый образец
config.example.toml коммитим как единый справочник по конфигу: все секции и
все поля. Каждое поле снабжаем комментарием, из которого ясно:
- зачем поле — что оно меняет в поведении;
- допустимые значения — перечисление или границы;
- единицы измерения, если применимы — секунды, байты, доля
0–1.
[server]
port = <N> # порт HTTP-сервера
shutdown_timeout = <N> # ждать мягкой остановки сервера, секунды
force_shutdown_timeout = <N> # ждать остановки воркеров, секунды
users_while_list = ["<@name>"] # кому отвечает бот; строка автора Telegram
Значения намеренно заменены плейсхолдерами: предмет конвенции — форма
комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и
начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут
одним домом — таблица «Настройки с числовым значением» в
../database.md; config.example.toml — источник истины по
составу полей.
Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»).
Расхождение: секции [server] в config.example.toml не хватает поля
users_while_list, из-за чего бот на свежем конфиге отвечает отказом всем.
Расхождение: адреса провайдера в секции [auth] образца заполнены примерами
вида https://auth.example.com/..., а не оставлены пустыми: пустой адрес не
говорит, какой формы значение здесь ждут. Пустым оставлен только
client_secret — он и есть секрет.
Поля по дискриминатору type
Когда набор полей секции зависит от поля-дискриминатора type (выбор одного из
бекендов или внешних сервисов), обязательность и опциональность полей определяет
значение type, а не фиксированный список секции.
- Проверка — по
type. Для каждого поддерживаемого значения свой набор обязательных полей; поля других значений не требуются. Неизвестное значение — ошибка на старте с перечислением поддерживаемых. - Образец — по
type. Вconfig.example.toml:- основное (умолчательное) значение предзаполнено рабочими значениями;
- альтернативные — блоками-комментариями ниже, каждый со своим описанием полей (зачем, границы, единицы — как у обычных полей);
- так из примера видны все варианты и поля каждого, не открывая код.
Дискриминатора в transcriber пока нет; правило записано на случай второго распознавателя.
Секреты
Секреты доставляет выкладка, рендеря их прямо в config.toml (transcriber:
Ansible из pet-project-server). Приложение просто читает TOML — отдельного слоя
секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не
репозиторий и не окружение.
- Секретные поля transcriber:
telegram.bot_token,yandex.speech_kit_api_key,yandex.object_storage_access_key_id,yandex.object_storage_secret_access_key,auth.client_secret. - Отрендеренный
config.toml(с секретами) не коммитится; права0600, владелец — пользователь процесса (1000:1000). - В
config.example.tomlсекретные поля — пустые строки. Расхождение: сейчас там стоят подсказки видаyour_..._here, а не пустые строки, и загрузчик их не отличает от настоящего значения. - Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Проверка и остановка на старте».
- В логи секреты не попадают — см. logging.md, «Безопасность».
- Отказ загрузки настроек не несёт содержимого файла. Текст такого отказа
собирает библиотека разбора, и собирает она его из разбираемого куска:
toml.ParseErrorкладёт в сообщение само значение («Invalid float value: %q»). Оборванная кавычка в строке секретного ключа — типовая поломка криво отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ разбора пересобирается своими словами: путь, строка, столбец и последний ключ, без сообщения библиотеки. Прочие отказы декодера (несовпадение типов, неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их текст остаётся как есть — иначе за разборчивость отказа платили бы там, где платить не за что.
Проверка и остановка на старте
Конфиг проверяем на старте, до приёма трафика. Негодный конфиг — лог ERROR
и выход с ненулевым кодом, не стартуем наполовину.
Что проверяем:
- обязательные поля заданы;
- каталоги хранилища существуют и доступны на запись;
- границы числовых полей соблюдены;
- ключи внешних сервисов не пусты.
Расхождение: LoadConfig проверяет только существование файла и разбирает
TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс
выходит с кодом 1. Единого места проверки нет.
Под это расхождение больше не подпадают два ключа секции [telegram] — признак
включения и ключ доступа, — и проверок у них две. Третий ключ секции,
update_timeout, границ по-прежнему не проверяет никто, и ноль в нём обращает
длинный опрос в непрерывный. Обязательность признака включения судит загрузчик — только разбор отличает
«ключ не задан» от «ключ задан ложным», потому что нулевое значение bool у
обоих одинаковое. Заполненность ключа доступа судит TelegramConfig.Validate() из
main.go, рядом с проверкой [auth]: пустой bot_token при enabled = true —
ошибка настройки и отказ старта. Непустой негодный по-прежнему судится при сборке
клиента, до подъёма сервера. Нормирует это openspec/specs/intake, «Признак
включения решает, поднимается ли вход Telegram».
Секция [auth] — первая, у которой проверка своя и стоит на старте:
AuthConfig.Validate() зовётся из main.go сразу после загрузки и роняет
процесс с перечнем незаполненных ключей. Причина в цене умолчания: поднявшись с
молча выключенным входом, сервис остался бы открытым наружу, а узнать об этом
было бы неоткуда. Сообщение называет имена ключей, а не значения — значение
client_secret в журнал попасть не должно.
Структура в коде
- Весь разбор и проверка — в
internal/config; наружу отдаётся готоваяConfig. - Одна корневая структура
Configс под-структурами по секциям. Перечень секций и полей здесь не повторяем: источник истины по составу —config.example.toml, действующие числа — ../database.md, «Настройки с числовым значением». Каталог данных задаётся одним ключом[storage] data_dir(ADR). - Умолчания задаются в
defaultConfig(), файл их перекрывает. Новое поле требует правки обоих мест. - Обязательное поле — поле, у которого умолчания нет намеренно. Умолчание у
такого поля было бы угаданным намерением, и одна из двух ошибок стала бы
тихой. Форма записи: умолчания нет ни в
defaultConfig()(причина — строкой комментария у самого поля), ни по нулевому значению типа; присутствие ключа судит разбор —MetaData.IsDefinedизtoml.DecodeFile, — потому что значение отличить «не задано» от «задано нулём» не позволяет. Вconfig.example.tomlу поля стоит значение свежей установки. Первое такое поле —telegram.enabled.