# План доработки: динамическое управление конфигурацией и очередью через API > Статус: **реализовано**. Документ фиксирует дизайн доработки Control > API, дающей возможность управлять `validators`, `sites`, > `check_types`/`targets` и очередью IP-адресов через HTTP API вместо > правки YAML + рестарта, без перезапуска процесса. Актуальная > спецификация методов — [docs/API.md](API.md#управление-очередью-и-конфигурацией); > повседневные сценарии — [docs/USAGE.md](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`. Целевой набор фич: 1. Администратор передаёт через API список IP-адресов на проверку. 2. Администратор передаёт через API список валидаторов. 3. Администратор передаёт через API список целей (`targets`/`check_types`). 4. Администратор может принудительно инициировать проверку адреса, даже если она уже была выполнена ранее. 5. Администратор может принудительно остановить идущую проверку. Все действия — «на лету», без перезапуска процесса. Источник истины после первого изменения через 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`: ```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` ```go func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error ``` Заменяет текущий цикл `RegisterValidator` + `SeedQueue` в `cmd/control-api/main.go`: - `ip_addresses` → `SeedQueue` — без изменений (всегда доливает новые адреса при каждом старте; отдельный YAML-only путь, не путать с runtime `POST /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.go` - `internal/db/queries_sites.go`, `queries_targetgroups.go`, `queries_checktypes.go`, `queries_validators.go`, `queries_ipqueue.go` - `internal/orchestrator/orchestrator.go` - `internal/httpapi/handlers_config.go`, `handlers_admin.go`, `dto_admin.go`, `routes.go` - `cmd/control-api/main.go` ## Проверка 1. `go build ./... && go test ./...` 2. `scripts/run-local-e2e.sh` 3. Ручная проверка: создать 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-изменения сохранились.