12 KiB
План доработки: динамическое управление конфигурацией и очередью через API
Статус: реализовано. Документ фиксирует дизайн доработки Control API, дающей возможность управлять
validators,sites,check_types/targetsи очередью IP-адресов через HTTP API вместо правки YAML + рестарта, без перезапуска процесса. Актуальная спецификация методов — docs/API.md; повседневные сценарии — docs/USAGE.md.
Context
Сейчас control-api в части конфигурации и очереди полностью read-only:
validators, sites, check_types/targets читаются один раз из YAML при
старте процесса (cmd/control-api/main.go) и живут в памяти
(Orchestrator.Checks, Orchestrator.Sites) либо переприменяются в БД при
каждом рестарте (RegisterValidator upsert). Список адресов на проверку
(ip_addresses) добавляется в очередь только при старте, и нет способа
принудительно перепроверить уже завершённый адрес или остановить проверку,
которая уже идёт. Единственный способ что-то поменять — отредактировать
YAML и выполнить systemctl restart control-api.
Целевой набор фич:
- Администратор передаёт через API список IP-адресов на проверку.
- Администратор передаёт через API список валидаторов.
- Администратор передаёт через API список целей (
targets/check_types). - Администратор может принудительно инициировать проверку адреса, даже если она уже была выполнена ранее.
- Администратор может принудительно остановить идущую проверку.
Все действия — «на лету», без перезапуска процесса. Источник истины после первого изменения через API — БД, YAML остаётся только bootstrap для пустой базы.
Согласованные решения:
-
Sites (площадки) включены в объём доработки — тем же CRUD-подходом, что и validators/targets/check_types.
-
Аутентификация (admin bearer-токен) НЕ входит в этот план — API остаётся открытым, как сейчас. Ограничение доступа — на уровне сети/firewall (см.
docs/SETUP.md). -
Фичи 1 и 4 реализуются одним механизмом, а не двумя разными эндпоинтами. Администратор передаёт список IP-адресов в
POST /api/v1/admin/ips; для каждого адреса в списке:- если адрес не встречался раньше — добавляется в очередь как новый;
- если адрес уже в терминальном состоянии (
done/failed) — принудительно перезапускается на проверку (сброс результата, новая попытка,attempt_numberувеличивается,retry_countобнуляется); - если адрес уже в очереди (
queued) — переупорядочивается под порядок текущего списка (без дублирования); - если адрес сейчас активно проверяется (
assigning_fip/awaiting_self_check/checking/aggregating) — не трогается вообще (не создаём вторую параллельную проверку одного и того же адреса).
Порядок обработки в рамках одного вызова соответствует порядку адресов в переданном списке — повторная отправка того же списка без изменений даёт тот же порядок прогона.
1. Схема БД — новая миграция + обобщение migrate()
internal/db/db.go: migrate() сейчас гейтится по PRAGMA user_version:
>=1 → no-op, иначе применяет единственный embedded migrations/0001_init.sql
и ставит user_version=1. Обобщается на упорядоченный список миграций
(embed 0002_dynamic_config.sql вторым файлом), применяются по очереди все
версии выше текущей.
Новый файл internal/db/migrations/0002_dynamic_config.sql:
CREATE TABLE sites (
idx INTEGER PRIMARY KEY, -- 1, 2 или 3 — фиксированный слот
site_id TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE target_groups (
group_name TEXT PRIMARY KEY,
targets TEXT NOT NULL, -- JSON-массив строк
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE check_types (
name TEXT PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT 1,
target_groups TEXT NOT NULL, -- JSON-массив имён групп
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
validators — существующая таблица (0001_init.sql), новых колонок не
требует.
2. Типизированные ошибки — internal/db/errors.go
Сентинелы (errors.New + %w-обёртка), чтобы httpapi-хендлеры маппили
их в HTTP-статусы через errors.Is: ErrNotFound (404), ErrConflict
(409), ErrBusy (409, валидатор владеет IP), ErrInUse (409, группа
целей используется check_type'ом), ErrValidation (400), ErrInvalidState
(409, попытка отменить уже завершённую проверку).
3. Bootstrap — internal/db/bootstrap.go
func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error
Заменяет текущий цикл RegisterValidator + SeedQueue в
cmd/control-api/main.go:
ip_addresses→SeedQueue— без изменений (всегда доливает новые адреса при каждом старте; отдельный YAML-only путь, не путать с runtimePOST /api/v1/admin/ips).validators,sites,target_groups,check_types— применяются только если соответствующая таблица пуста. Если строки уже есть — YAML для этой секции игнорируется.
Осознанное изменение поведения: сейчас RegisterValidator при каждом
рестарте переприменяет os_port_id из YAML поверх БД. После доработки —
только на пустой таблице (иначе API-правки не переживали бы рестарт).
4. Запросы к БД
internal/db/queries_sites.go:ListSites,UpsertSite(idx, siteID),DeleteSite(idx),GetSiteIndex(ctx, siteID) (int, error)(0, если не найден — не ошибка).internal/db/queries_targetgroups.go:ListTargetGroups,UpsertTargetGroup(name, targets),DeleteTargetGroup(name)(ErrInUse, если ссылается check_type),GetTargetGroup(name).internal/db/queries_checktypes.go:ListCheckTypes,ListResolvedCheckTypes(разворачивает группы в плоский список targets),UpsertCheckType(name, enabled, targetGroups)(ErrValidation, если группа не существует),DeleteCheckType(name).internal/db/queries_validators.go(дополнить):AdminCreateValidator,AdminUpdateValidatorPort,DeleteValidator(ErrBusy, если владеет IP).internal/db/queries_ipqueue.go(дополнить):SubmitIPs(ctx, addresses []string) (SubmitIPsResult, error)— единая транзакция, реализует правило из Context выше;CancelIP(ctx, ipID int64) error— условныйUPDATE ... WHERE state NOT IN ('done','failed'),ErrInvalidStateпри гонке/уже завершённой проверке.
ResultCancelled = "cancelled" добавляется в internal/db/models.go.
5. Оркестратор
internal/orchestrator/orchestrator.go: убрать статические поля
Checks/Sites, читать динамически из БД (ListResolvedCheckTypes,
ListSites, GetSiteIndex) в AssignmentForValidator,
expectedCheckCount(), isReadyToAggregate(). Новый метод ForceCancel
для фичи 5: отвязывает FIP (best-effort), помечает IP cancelled,
освобождает валидатора.
Принятый компромисс: если конфигурация меняется API-запросом ровно в
момент агрегации уже идущей проверки, эта попытка агрегирует по текущей
(уже изменённой) конфигурации — деградирует безопасно через
missing_counts_as_fail, самоисправляется на следующей попытке.
6. HTTP API
POST /api/v1/admin/ips — постановка/принудительный перезапуск (фичи 1 и
4). POST /api/v1/admin/ips/{ip}/cancel — остановка (фича 5).
/api/v1/admin/config/{validators,sites,targets,check-types} — CRUD
(фичи 2 и 3), snake_case DTO. Подробности — docs/API.md.
Критичные файлы
internal/db/migrations/0002_dynamic_config.sql,internal/db/db.go,internal/db/bootstrap.go,internal/db/errors.go,internal/db/models.gointernal/db/queries_sites.go,queries_targetgroups.go,queries_checktypes.go,queries_validators.go,queries_ipqueue.gointernal/orchestrator/orchestrator.gointernal/httpapi/handlers_config.go,handlers_admin.go,dto_admin.go,routes.gocmd/control-api/main.go
Проверка
go build ./... && go test ./...scripts/run-local-e2e.sh- Ручная проверка: создать validator/site/target-group/check-type только
через API;
POST /admin/ipsс ужеdone-адресом → повторный полный цикл;POST /admin/ipsс адресом вchecking→ не трогается;POST /admin/ips/{ip}/cancelво времяchecking→ FIP отвязан, валидатор свободен,overall_result=cancelled; удаление занятого валидатора → 409; рестарт control-api → все API-изменения сохранились.