2026-08-23 20:39:22 +03:00
|
|
|
|
# План доработки: динамическое управление конфигурацией и очередью через API
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
> Статус: **реализовано**. Документ фиксирует дизайн доработки Control
|
|
|
|
|
|
> API, дающей возможность управлять `validators`, `sites`,
|
|
|
|
|
|
> `check_types`/`targets` и очередью IP-адресов через HTTP API вместо
|
|
|
|
|
|
> правки YAML + рестарта, без перезапуска процесса. Актуальная
|
|
|
|
|
|
> спецификация методов — [docs/API.md](API.md#управление-очередью-и-конфигурацией);
|
|
|
|
|
|
> повседневные сценарии — [docs/USAGE.md](USAGE.md).
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Сейчас `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`.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Целевой набор фич:
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
1. Администратор передаёт через API список IP-адресов на проверку.
|
|
|
|
|
|
2. Администратор передаёт через API список валидаторов.
|
|
|
|
|
|
3. Администратор передаёт через API список целей (`targets`/`check_types`).
|
|
|
|
|
|
4. Администратор может принудительно инициировать проверку адреса, даже
|
|
|
|
|
|
если она уже была выполнена ранее.
|
|
|
|
|
|
5. Администратор может принудительно остановить идущую проверку.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Все действия — «на лету», без перезапуска процесса. Источник истины после
|
|
|
|
|
|
первого изменения через API — БД, YAML остаётся только bootstrap для пустой
|
|
|
|
|
|
базы.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
**Согласованные решения:**
|
|
|
|
|
|
- **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`) — не трогается
|
|
|
|
|
|
вообще (не создаём вторую параллельную проверку одного и того же
|
|
|
|
|
|
адреса).
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Порядок обработки в рамках одного вызова соответствует порядку адресов в
|
|
|
|
|
|
переданном списке — повторная отправка того же списка без изменений даёт
|
|
|
|
|
|
тот же порядок прогона.
|
|
|
|
|
|
|
|
|
|
|
|
## 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`:
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
`validators` — существующая таблица (`0001_init.sql`), новых колонок не
|
|
|
|
|
|
требует.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## 2. Типизированные ошибки — `internal/db/errors.go`
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Сентинелы (`errors.New` + `%w`-обёртка), чтобы `httpapi`-хендлеры маппили
|
|
|
|
|
|
их в HTTP-статусы через `errors.Is`: `ErrNotFound` (404), `ErrConflict`
|
|
|
|
|
|
(409), `ErrBusy` (409, валидатор владеет IP), `ErrInUse` (409, группа
|
|
|
|
|
|
целей используется check_type'ом), `ErrValidation` (400), `ErrInvalidState`
|
|
|
|
|
|
(409, попытка отменить уже завершённую проверку).
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## 3. Bootstrap — `internal/db/bootstrap.go`
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
Заменяет текущий цикл `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
|
|
|
|
|
|
для этой секции игнорируется.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
**Осознанное изменение поведения**: сейчас `RegisterValidator` при каждом
|
|
|
|
|
|
рестарте переприменяет `os_port_id` из YAML поверх БД. После доработки —
|
|
|
|
|
|
только на пустой таблице (иначе API-правки не переживали бы рестарт).
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## 4. Запросы к БД
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
- `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` при гонке/уже завершённой проверке.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
`ResultCancelled = "cancelled"` добавляется в `internal/db/models.go`.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## 5. Оркестратор
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
`internal/orchestrator/orchestrator.go`: убрать статические поля
|
|
|
|
|
|
`Checks`/`Sites`, читать динамически из БД (`ListResolvedCheckTypes`,
|
|
|
|
|
|
`ListSites`, `GetSiteIndex`) в `AssignmentForValidator`,
|
|
|
|
|
|
`expectedCheckCount()`, `isReadyToAggregate()`. Новый метод `ForceCancel`
|
|
|
|
|
|
для фичи 5: отвязывает FIP (best-effort), помечает IP `cancelled`,
|
|
|
|
|
|
освобождает валидатора.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
**Принятый компромисс**: если конфигурация меняется API-запросом ровно в
|
|
|
|
|
|
момент агрегации уже идущей проверки, эта попытка агрегирует по текущей
|
|
|
|
|
|
(уже изменённой) конфигурации — деградирует безопасно через
|
|
|
|
|
|
`missing_counts_as_fail`, самоисправляется на следующей попытке.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## 6. HTTP API
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
`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`.
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
|
|
|
|
|
## Критичные файлы
|
|
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
- `internal/db/migrations/0002_dynamic_config.sql`, `internal/db/db.go`,
|
|
|
|
|
|
`internal/db/bootstrap.go`, `internal/db/errors.go`, `internal/db/models.go`
|
2026-08-21 09:58:08 +03:00
|
|
|
|
- `internal/db/queries_sites.go`, `queries_targetgroups.go`,
|
2026-08-23 20:39:22 +03:00
|
|
|
|
`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`
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
## Проверка
|
2026-08-21 09:58:08 +03:00
|
|
|
|
|
2026-08-23 20:39:22 +03:00
|
|
|
|
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-изменения сохранились.
|