Files
cloud-ip-validator/docs/PLAN_API_CONFIG_MANAGEMENT.md
T

188 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План доработки: динамическое управление конфигурацией и очередью через 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-изменения сохранились.