Files
cloud-ip-validator/docs/PLAN_API_CONFIG_MANAGEMENT.md
T
2026-08-21 09:58:08 +03:00

22 KiB
Raw Blame History

План доработки: API управления конфигурацией

Статус: план на будущее, не реализовано. Документ фиксирует согласованный дизайн доработки Control API, дающей возможность управлять check_types, targets, validators и sites через HTTP API вместо правки YAML + рестарта. Реализация — отдельная задача.

Context

Сейчас control-api полностью read-only в части конфигурации: типы проверок (check_types), список целей (targets), состав валидаторов (validators) и внешних площадок (sites) читаются один раз из control-api.yaml при старте процесса и живут дальше только в памяти (Orchestrator.Checks, Orchestrator.Sites) либо (для валидаторов) в таблице validators, куда при каждом рестарте они переупорядочиваются из YAML. Единственный способ что-то поменять — отредактировать YAML и выполнить systemctl restart control-api. Это задокументированное ограничение (см. docs/USAGE.md).

Согласованные решения (зафиксированы для будущей реализации):

  • Область доработки — ровно четыре сущности: check_types (типы проверок + их привязка к группам целей), targets (группы целей), validators (состав ВМ-валидаторов), sites (состав из ≤3 внешних площадок). Очередь ip_addresses и тайминги оркестратора (orchestrator.*, aggregation.*, inbound_checks.*) — вне scope, остаются YAML-only как сейчас.
  • Источник истины после первого изменения — БД. YAML используется только для одноразового bootstrap при пустой базе; после первого запуска (или после первого API-изменения) YAML для этих 4 секций больше не перечитывается и не переприменяется при рестартах.
  • Добавляется базовая аутентификация — bearer-токен администратора, которым закрывается весь namespace /api/v1/admin/* (не только новые write-методы, но и существующие read-методы status/ips/validators — единая политика для всего admin-namespace проще и логичнее половинчатой защиты). Протокол /api/v1/agents/* и /api/v1/probers/* (agent/prober) — вне scope, остаётся как есть.

Важное архитектурное ограничение: площадки жёстко капнуты на 3

Схема ip_queue хранит завершённость площадок как три отдельные колонки (site1_complete, site2_complete, site3_complete) — это не список произвольной длины. Поэтому API для sites не может быть обычным CRUD-списком: это управление максимум тремя пронумерованными слотами (index ∈ {1,2,3}), где site_id можно назначить, переименовать или снять со слота. Это ограничение уже описано в docs/USAGE.md и явно закладывается в дизайн API ниже, а не игнорируется.

Общий план реализации

1. Новая схема БД — 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
);

Список целей внутри группы и список групп внутри типа проверки хранятся как JSON-массив в TEXT-колонке (тот же паттерн, что уже используется для events.payload) — они всегда читаются/пишутся целиком, отдельная реляционная таблица тут не нужна (не переусложняем).

validators — существующая таблица, новых колонок не требует.

internal/db/db.go: функцию migrate() обобщить со списка из одной миграции (version >= 1 → return) на упорядоченный список {version, sql} и применение всех версий выше текущего PRAGMA user_version — понадобится и для этой, и для будущих миграций.

2. Bootstrap-логика — новый файл internal/db/bootstrap.go

func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error

Переносит и обобщает то, что сейчас разбросано по cmd/control-api/main.go (RegisterValidator в цикле + SeedQueue):

  • ip_addresses → SeedQueue — без изменений, как сейчас (всегда доливает новые адреса, это уже вне scope доработки).
  • validators, sites, target_groups, check_types — новая семантика: применяется, только если соответствующая таблица сейчас пуста (SELECT COUNT(*) ... == 0). Если в таблице уже есть строки — YAML для этой секции полностью игнорируется, ничего не трогаем. Это и есть «bootstrap один раз, дальше БД главная».

internal/db уже не будет зависеть от internal/orchestrator — только новая зависимость internal/db → internal/config (обратной зависимости config → db нет, циклов не возникает).

cmd/control-api/main.go: заменить текущий цикл RegisterValidator + SeedQueue одним вызовом database.BootstrapFromConfig(ctx, cfg). Это же делает функцию тестируемой напрямую (используется в обновлённых orchestrator_test.go/httpapi_test.go вместо ручного построения Orchestrator.Checks/.Sites).

Важное следствие смены семантики валидаторов: сейчас при каждом рестарте control-api валидаторы из YAML переприменяются (в частности, может тихо откатить os_port_id, изменённый через API/вручную в БД). После доработки — только на пустой таблице. Это осознанное поведенческое изменение, требует апдейта docs/SETUP.md/docs/USAGE.md (шаг 6 плана).

3. Запросы к БД для новых сущностей

Новые файлы, по аналогии с существующими queries_*.go:

  • internal/db/queries_sites.go: ListSites, UpsertSite(idx, siteID) (проверяет допустимость idx 1..3 и уникальность site_id до записи, чтобы вернуть чистую типизированную ошибку, а не сырую SQL), DeleteSite(idx), GetSiteIndex(siteID) (int, error) — заменяет текущий Orchestrator.SiteIndexForID, который сканирует статический слайс.
  • internal/db/queries_targetgroups.go: ListTargetGroups, UpsertTargetGroup(name, targets), DeleteTargetGroup(name) — перед удалением проверяет, что ни один check_types не ссылается на эту группу (иначе ErrInUse), GetTargetGroup(name).
  • internal/db/queries_checktypes.go: ListCheckTypes, ListResolvedCheckTypes (сразу разворачивает имена групп в плоский список URL — то, что раньше строил orchestrator.New() один раз при старте), UpsertCheckType(name, enabled, targetGroups) (проверяет, что все переданные targetGroups существуют — иначе ErrValidation), DeleteCheckType(name).
  • internal/db/queries_validators.go (дополнить существующий файл): AdminCreateValidator(id, osPortID) (409/ErrConflict, если уже есть), AdminUpdateValidatorPort(id, osPortID) (404/ErrNotFound, если нет), DeleteValidator(id) (409/ErrBusy, если current_ip_id IS NOT NULL — валидатор сейчас владеет IP). Не путать с существующим RegisterValidator — тот остаётся as-is и продолжает использоваться только агентом при самостоятельной регистрации (handleAgentRegister), полей os_port_id не трогает при self-registration (это уже так в текущем коде).

Новый файл internal/db/errors.go с типизированными сентинелами (ErrNotFound, ErrConflict, ErrBusy, ErrValidation, через errors.New

  • %w-обёртку в местах возврата) — чтобы httpapi-хендлеры мапили их в 404/409/400 через errors.Is, а не всё подряд в 500 (как сейчас местами получается по умолчанию).

4. Оркестратор — переход на динамическое чтение конфигурации

internal/orchestrator/orchestrator.go:

  • Убрать поля Checks []CheckConfig и Sites []config.SiteConfig из Orchestrator (сейчас вычисляются один раз в New() и застывают на весь жизненный цикл процесса — это и есть корень проблемы). Inbound остаётся статическим полем как сейчас (вне scope).
  • AssignmentForValidator — вместо return item, o.Checks, nil вызывает o.DB.ListResolvedCheckTypes(ctx) и возвращает актуальный на данный момент список.
  • SiteIndexForID — удаляется, вызовы (handleProberRegister, handleProberAssignments, handleProberResults) переходят на o.DB.GetSiteIndex(ctx, siteID).
  • expectedCheckCount() — читает актуальные ListResolvedCheckTypes и ListSites из БД на момент агрегации, а не статические поля.

Принятый компромисс (осознанно, без over-engineering): если check_types/targets/sites меняются API-запросом ровно в момент, когда чей-то IP уже находится в checking (self-check уже пройден, проверки уже назначены агенту), агрегация этой конкретной попытки посчитает текущую (уже изменённую) конфигурацию, а не ту, что была на момент выдачи задания. На практике это узкое окно в несколько секунд между админ-изменением и завершением проверки; деградирует безопасно — через существующий механизм missing_counts_as_fail результат в худшем случае будет partial вместо pass для одной попытки, самоисправляется на следующей (после retry/requeue). Полный snapshot-per-attempt (доп. колонки в ip_queue с зафиксированным ожидаемым числом проверок) — возможное будущее усиление, не требуется для этой доработки.

5. HTTP API

Новый файл internal/httpapi/handlers_config.go и DTO в dto.go. Все — под префиксом /api/v1/admin/config/*, JSON в snake_case (в отличие от существующих /admin/status|ips|validators, которые отдают сырые Go-поля в PascalCase — для новых, «настоящих» management-эндпоинтов сразу делаем нормальный контракт, старые не трогаем, чтобы не ломать уже задокументированное поведение).

Метод Путь Тело Успех Ошибки
GET /api/v1/admin/config/validators — [{validator_id, os_port_id, state}]
POST /api/v1/admin/config/validators {validator_id, os_port_id} 201 409 если уже есть
PUT /api/v1/admin/config/validators/{id} {os_port_id} 200 404
DELETE /api/v1/admin/config/validators/{id} — 200 404, 409 если владеет IP
GET /api/v1/admin/config/sites — [{index, site_id}] (до 3 строк)
PUT /api/v1/admin/config/sites/{index} {site_id} 200 400 если index не 1..3, 409 если site_id занят другим слотом
DELETE /api/v1/admin/config/sites/{index} — 200 404
GET /api/v1/admin/config/targets — [{name, targets}]
PUT /api/v1/admin/config/targets/{group} {targets:[...]} 200 400 пустой список
DELETE /api/v1/admin/config/targets/{group} — 200 404, 409 если используется check_type'ом
GET /api/v1/admin/config/check-types — [{name, enabled, targets}]
PUT /api/v1/admin/config/check-types/{name} {enabled, targets:[group,...]} 200 400 если группа не существует
DELETE /api/v1/admin/config/check-types/{name} — 200 404

routes.go: все существующие и новые /api/v1/admin/*-маршруты оборачиваются s.requireAdmin(...).

6. Аутентификация

  • internal/config/config.go: в ServerConfig добавить AdminTokenEnv string \yaml:"admin_token_env"`— по аналогии сopenstack.*_env` полями (в YAML — только имя переменной, не сам токен).
  • internal/httpapi/server.go: Server.AdminToken string + func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc — сверяет Authorization: Bearer <token> через crypto/subtle.ConstantTimeCompare. Если s.AdminToken == "" — пропускает без проверки (обратная совместимость).
  • cmd/control-api/main.go: если cfg.Server.AdminTokenEnv задан, но os.Getenv(...) пуст — отказ запуска с понятной ошибкой (fail-safe, не запускаемся с «пустым паролем»). Если AdminTokenEnv вообще не задан — запускаемся как сейчас, но пишем явный log.Warn про незащищённый admin API.
  • configs/control-api.example.yaml, deploy/systemd/control-api.service (добавить пример переменной в EnvironmentFile) — обновить.

7. Обновление существующих тестов и добавление новых

  • internal/orchestrator/orchestrator_test.go, internal/httpapi/httpapi_test.go: заменить ручное построение cfg.CheckTypes/.Targets/.Sites + прямые поля Orchestrator{Checks:...} на db.BootstrapFromConfig(ctx, cfg) перед orchestrator.New(...) — сами тестовые сценарии (happy path, partial, lease reclaim) не меняются по сути, меняется только способ засеять конфигурацию.
  • Новые unit-тесты: internal/db/queries_dynconfig_test.go (CRUD + граничные случаи: удаление занятого валидатора → ErrBusy, удаление группы целей, на которую ссылается check_type → ErrInUse, upsert check_type с несуществующей группой → ErrValidation, upsert сайта с чужим site_id → ErrConflict, bootstrap на непустой таблице → YAML игнорируется).
  • Новый internal/httpapi/handlers_config_test.go (или расширение httpapi_test.go): сквозной сценарий — создать валидатора и сайт через API вместо конфига, убедиться, что IP реально дошёл до done; смена check_types между запусками влияет на следующий назначенный IP.
  • Обновить scripts/run-local-e2e.sh не требуется по сути (bootstrap из YAML при пустой БД работает как раньше), но стоит добавить один шаг с curl -X PUT .../config/check-types/ssh как живую демонстрацию.

8. Документация (после реализации)

  • docs/API.md: новый раздел «Методы управления конфигурацией» с таблицей выше + примеры curl (создание валидатора, отключение ssh, добавление цели, назначение площадки на слот) + раздел про Authorization: Bearer.
  • docs/SETUP.md: шаг про server.admin_token_env в «Переменные окружения для OpenStack» (переименовать раздел или добавить рядом «и для admin-токена»); явно описать новую bootstrap-once семантику validators/sites/check_types/targets.
  • docs/USAGE.md: заменить текущие разделы «Управление валидаторами» / «Управление площадками» (сейчас там «только через YAML + restart») на актуальные — через API; убрать утверждение «нет API-метода» там, где оно перестало быть верным.
  • docs/DIAGRAMS.md: в диаграмму control plane (раздел 1) добавить новую стрелку «Оператор → HTTP API → БД (config CRUD)» вместо текущей «CFG → читается при старте (инициализация)» как единственного пути.

Критичные файлы

  • internal/db/migrations/0002_dynamic_config.sql (новый)
  • internal/db/db.go (обобщить migrate())
  • internal/db/bootstrap.go (новый)
  • internal/db/errors.go (новый)
  • internal/db/queries_sites.go, queries_targetgroups.go, queries_checktypes.go (новые), queries_validators.go (дополнить)
  • internal/orchestrator/orchestrator.go (убрать статические Checks/Sites, читать из БД)
  • internal/httpapi/handlers_config.go (новый), dto.go, routes.go, server.go (requireAdmin)
  • internal/config/config.go (AdminTokenEnv)
  • cmd/control-api/main.go (bootstrap-вызов, проверка токена при старте)

Проверка (когда план будет реализовываться)

  1. go build ./... && go test ./... — все существующие + новые unit- и httpapi-тесты проходят.
  2. scripts/run-local-e2e.sh — офлайн-сценарий по-прежнему проходит от начала до конца без ручного вмешательства (bootstrap из YAML при пустой БД работает как раньше).
  3. Ручная проверка нового контракта: поднять control-api с пустой БД и admin_token_env без токена → админ-запрос без заголовка проходит; задать токен → запрос без Authorization получает 401; создать валидатора/площадку/группу целей/тип проверки через API без единой строчки в YAML, убедиться, что IP реально проходит полный цикл проверки на этой конфигурации; попытаться удалить валидатора, пока он владеет IP → 409; перезапустить control-api и убедиться, что API-изменения пережили рестарт, а YAML их не затёр.