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

12 KiB
Raw Blame History

План доработки: динамическое управление конфигурацией и очередью через 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.

Целевой набор фич:

  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:

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 путь, не путать с 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-изменения сохранились.