feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
+213
-9
@@ -10,6 +10,20 @@ info:
|
||||
**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.
|
||||
Ошибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
|
||||
Пагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.
|
||||
**Общий предел request body:** после аутентификации каждый непубличный `/v1/...`
|
||||
запрос проверяется целиком до выполнения handler: максимум 8 MiB (8388608 bytes),
|
||||
независимо от HTTP-метода, Content-Type и наличия Content-Length. Превышение,
|
||||
включая whitespace после JSON, возвращает `413 application/problem+json`
|
||||
до мутаций. Ошибка чтения тела возвращает `400`, отмена/таймаут чтения — `408`.
|
||||
Пустые node GET/pull и streaming SSE responses не меняются. CSV дополнительно
|
||||
сохраняет ограничения числа строк и полей своей операции.
|
||||
Запросы с телом занимают ограниченный слот API instance до окончания handler:
|
||||
по умолчанию 4 одновременно, `EVOBGP_API_BODY_MAX_CONCURRENT` задаёт 1..64.
|
||||
Свободных слотов нет — немедленный `429` с `Retry-After: 1`, без очереди ожидания.
|
||||
Пустые запросы (`http.NoBody`) слота не занимают.
|
||||
Если CRUD-изменение сохранено, но автоматическая refresh/reconcile задача не поставлена,
|
||||
успешный ответ содержит `X-EvoBGP-Job-Status: enqueue-failed`; ошибка также попадает в аудит.
|
||||
Изменение уже сохранено: следует явно запустить refresh/reconcile, а не повторять создание объекта.
|
||||
|
||||
**Аутентификация (dual):**
|
||||
- **API key** — `Authorization: Bearer <token>` из `EVOBGP_API_KEYS` / таблицы `api_key` (роли `viewer`/`editor`/`operator`/`node`/`firewall`).
|
||||
@@ -226,6 +240,16 @@ components:
|
||||
Основные значения: `module_refresh`, `peer_reconcile`, `deploy_apply`, `revision_rollback`, `bird_reload`.
|
||||
|
||||
responses:
|
||||
ExpensiveOperationLimited:
|
||||
description: Локальная квота процесса API — по умолчанию 4 активных preview/lookup, 2 на tenant; 120 lookup и по 30 CDN/revision preview в минуту на tenant. Квоты умножаются на число реплик. Повторить после Retry-After, без очереди ожидания.
|
||||
headers:
|
||||
Retry-After:
|
||||
description: Минимальная задержка повторной попытки в секундах; не гарантия свободного места.
|
||||
schema: {type: integer, minimum: 1}
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Problem"
|
||||
Unauthorized:
|
||||
description: Нет или невалидный Bearer-токен.
|
||||
content:
|
||||
@@ -263,7 +287,10 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Problem"
|
||||
DefaultProblem:
|
||||
description: Ошибка (см. тело Problem).
|
||||
description: >
|
||||
Ошибка (см. тело Problem). Общие для authenticated `/v1` ответы middleware:
|
||||
413 при request body свыше 8 MiB, 400 при ошибке чтения тела, 408 при отмене/таймауте чтения.
|
||||
429 с Retry-After: 1 при занятых слотах обработки request body.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
@@ -903,6 +930,21 @@ components:
|
||||
connected_speaker_label:
|
||||
type: string
|
||||
description: Человекочитаемая метка ноды из live-опроса.
|
||||
last_known_session_state:
|
||||
type: ["string", "null"]
|
||||
description: Последнее сохранённое состояние; не является текущим наблюдением.
|
||||
session_state:
|
||||
type: ["string", "null"]
|
||||
description: >
|
||||
При live-ответе текущее наблюдение: BGP state, absent после успешного
|
||||
пустого опроса или null, если актуальное состояние неизвестно.
|
||||
live_observation:
|
||||
$ref: "#/components/schemas/LiveObservation"
|
||||
description: >
|
||||
Качество BGP-наблюдений при GET /v1/peers?live=1. Для назначенного
|
||||
пира учитывается только bgp_speaker_id; для общего — все speakers.
|
||||
Смешанный успех и сбой не дают quality=ok или session_state=absent.
|
||||
Timestamps относятся только к ожидаемым speakers.
|
||||
session_on_speakers:
|
||||
type: array
|
||||
description: Состояние протокола пира на каждой опрошенной ноде.
|
||||
@@ -915,6 +957,10 @@ components:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
poll_error:
|
||||
type: string
|
||||
observation:
|
||||
$ref: "#/components/schemas/LiveObservation"
|
||||
established_on_speakers:
|
||||
type: array
|
||||
description: Ноды, где сессия в состоянии Established (один пир может быть на нескольких).
|
||||
@@ -1001,7 +1047,11 @@ components:
|
||||
description: Человекочитаемая метка ноды (agent domain или CP master).
|
||||
agent_ok:
|
||||
type: boolean
|
||||
description: true если agent /v1/agent/health успешен (master — local birdc poll).
|
||||
description: true только после успешного agent /v1/agent/health (master — local birdc poll).
|
||||
agent_observation:
|
||||
$ref: "#/components/schemas/LiveObservation"
|
||||
bgp_observation:
|
||||
$ref: "#/components/schemas/LiveObservation"
|
||||
agent_error:
|
||||
type: string
|
||||
agent_last_sync_at:
|
||||
@@ -1015,15 +1065,49 @@ components:
|
||||
bgp_poll_error:
|
||||
type: string
|
||||
bgp_sessions_total:
|
||||
type: integer
|
||||
type: ["integer", "null"]
|
||||
description: null при отсутствии успешного опроса.
|
||||
bgp_established:
|
||||
type: integer
|
||||
type: ["integer", "null"]
|
||||
description: null при отсутствии успешного опроса.
|
||||
sessions:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/BgpSessionLive"
|
||||
additionalProperties: true
|
||||
|
||||
LiveObservation:
|
||||
type: object
|
||||
description: >
|
||||
Метаданные конкретного опроса. observed_at — время завершения опроса;
|
||||
received_at — время получения результата CP; last_success_at — время
|
||||
последнего успешного опроса. Старые агенты не обязаны передавать эти поля:
|
||||
CP формирует их по собственным часам. При отсутствии опроса timestamps null.
|
||||
required: [observed_at, received_at, last_success_at, source, quality]
|
||||
properties:
|
||||
observed_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
received_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
last_success_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
description: Срок актуальности snapshot по часам CP и настроенному интервалу collector; клиент не должен считать cached quality=ok актуальным после этого времени. Для агрегата — самый ранний срок источников.
|
||||
source:
|
||||
type: string
|
||||
enum: [none, cp_birdc, agent_bird_protocols, agent_health, mixed]
|
||||
quality:
|
||||
type: string
|
||||
enum: [ok, stale, unreachable, unknown, disabled]
|
||||
error_code:
|
||||
type: string
|
||||
enum: [not_configured, not_polled, poll_failed, reported_unhealthy, snapshot_expired, configuration_changed, ambiguous_authority]
|
||||
|
||||
BgpSessionLive:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1052,9 +1136,12 @@ components:
|
||||
ok:
|
||||
type: boolean
|
||||
session_count:
|
||||
type: integer
|
||||
type: ["integer", "null"]
|
||||
description: null, если успешного опроса не было.
|
||||
poll_error:
|
||||
type: string
|
||||
observation:
|
||||
$ref: "#/components/schemas/LiveObservation"
|
||||
additionalProperties: true
|
||||
|
||||
PostgresOverview:
|
||||
@@ -2108,6 +2195,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LookupResponse"
|
||||
"429":
|
||||
$ref: "#/components/responses/ExpensiveOperationLimited"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
@@ -2115,6 +2204,29 @@ paths:
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/telemetry/refresh:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
post:
|
||||
tags: [Speakers]
|
||||
summary: Запросить фоновое обновление телеметрии tenant
|
||||
description: Требует bgp:network:read. Объединяет повторные запросы в пределах 10 секунд; HTTP handler не опрашивает агентов или BIRD. GET speakers/peers/discoveries читает сохранённые snapshots, включая live=1. Запрос во время выполняющегося poll сохраняется для следующего цикла. Не изменяет конфигурацию speaker и не требует нового протокола агента.
|
||||
operationId: requestTelemetryRefresh
|
||||
responses:
|
||||
"202":
|
||||
description: Запрос принят либо объединён с уже принятым.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [requested]
|
||||
properties:
|
||||
requested:
|
||||
type: boolean
|
||||
description: true при постановке нового запроса; false при объединении или отсутствии целей.
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/router-lists/catalog:
|
||||
get:
|
||||
tags: [Modules]
|
||||
@@ -2789,6 +2901,9 @@ paths:
|
||||
- `ipRange,community`
|
||||
|
||||
В поле `community` можно передавать либо ID community, либо её значение.
|
||||
Предел запроса — 8 MiB (8388608 байт), 10000 строк данных без заголовка и 64 поля в записи.
|
||||
Превышение возвращает 413 до создания записей; большой импорт следует разбить на части.
|
||||
Ошибка в данных или БД после начала создания записей может оставить частичный импорт.
|
||||
operationId: importModuleEntriesCsv
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
@@ -2812,6 +2927,12 @@ paths:
|
||||
minimum: 0
|
||||
module_type:
|
||||
type: string
|
||||
"413":
|
||||
description: Превышен лимит байт, строк или полей CSV; записи не созданы.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Problem"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"422":
|
||||
@@ -2898,6 +3019,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CdnPreviewResponse"
|
||||
"429":
|
||||
$ref: "#/components/responses/ExpensiveOperationLimited"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
@@ -3146,7 +3269,7 @@ paths:
|
||||
type: string
|
||||
enum: ["1"]
|
||||
description: >
|
||||
Опрос birdc на CP и GET /v1/agent/bird/protocols на репликах; обогащает session_state и connected_speaker_*.
|
||||
Сохранённые результаты фонового опроса birdc на CP и agent protocols на репликах; обогащает session_state и connected_speaker_*. GET не запускает опрос; live=1 сохранён для совместимости.
|
||||
responses:
|
||||
"200":
|
||||
description: Успешно.
|
||||
@@ -3167,7 +3290,7 @@ paths:
|
||||
live_speaker_poll:
|
||||
type: array
|
||||
description: >
|
||||
При live=1 — результат опроса каждой ноды (CP birdc + agent protocols).
|
||||
Сохранённый результат фонового опроса каждой ноды (CP birdc + agent protocols); live=1 не запускает новый опрос.
|
||||
items:
|
||||
$ref: "#/components/schemas/LiveSpeakerPoll"
|
||||
default:
|
||||
@@ -3204,7 +3327,7 @@ paths:
|
||||
summary: Список обнаруженных (неодобренных) пиров
|
||||
description: >
|
||||
Dynamic BGP-сессии (`evobgp_dyn_*`), которых ещё нет в `bgp_peer`.
|
||||
По умолчанию возвращает `status=pending`. При листинге выполняет live-опрос birdc/agent и upsert pending.
|
||||
По умолчанию возвращает `status=pending`. Читает сохранённые discoveries. Фоновый collector выполняет опрос и fenced upsert; GET не опрашивает ноды и не изменяет данные.
|
||||
operationId: listDiscoveredPeers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
@@ -3380,7 +3503,7 @@ paths:
|
||||
type: string
|
||||
enum: ["1"]
|
||||
description: >
|
||||
Live-опрос agent /v1/agent/health и BGP protocols на репликах; CP — local birdc.
|
||||
Сохранённые результаты фоновых опросов agent health/BGP и local birdc. GET не запускает опрос.
|
||||
Обогащает каждый item полем `live`.
|
||||
responses:
|
||||
"200":
|
||||
@@ -3640,6 +3763,8 @@ paths:
|
||||
description: Только чтение, без apply.
|
||||
operationId: getRevisionPreview
|
||||
responses:
|
||||
"429":
|
||||
$ref: "#/components/responses/ExpensiveOperationLimited"
|
||||
"200":
|
||||
description: Текст или структурированное представление - формат задаётся реализацией.
|
||||
content:
|
||||
@@ -3881,6 +4006,58 @@ paths:
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/jobs/{job_id}/report:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
get:
|
||||
tags: [Jobs]
|
||||
summary: Отчёт задачи и сохранённый прогресс доставки
|
||||
description: Требует bgp:operations:read. Прогресс хранится независимо от meta задачи. Для reconciliation без собственного прогресса возвращается прогресс исходной задачи в том же tenant; после начала восстановления приоритет имеет собственный прогресс. agent_reported не означает проверку состояния BIRD.
|
||||
operationId: getJobReport
|
||||
responses:
|
||||
"200":
|
||||
description: Успешно.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [job_id, kind, status, deployment_progress]
|
||||
properties:
|
||||
job_id: {type: string}
|
||||
kind: {type: string}
|
||||
status: {type: string}
|
||||
meta: {type: [object, "null"], additionalProperties: true}
|
||||
error: {type: [string, "null"]}
|
||||
created_at: {type: string, format: date-time}
|
||||
revision_id: {type: string}
|
||||
log_entries: {type: array, items: {type: object, additionalProperties: true}}
|
||||
log_total: {type: integer}
|
||||
source_deployment_job_id: {type: string}
|
||||
deployment_progress:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [job_id, speaker_id, revision_id, generation, state, confirmation, dispatch_attempts, retry_round]
|
||||
properties:
|
||||
job_id: {type: string}
|
||||
speaker_id: {type: string}
|
||||
revision_id: {type: string}
|
||||
generation: {type: integer, format: int64}
|
||||
state: {type: string}
|
||||
confirmation: {type: string}
|
||||
error: {type: string}
|
||||
dispatch_attempts: {type: integer}
|
||||
retry_round: {type: integer}
|
||||
health_received_at: {type: [string, "null"], format: date-time}
|
||||
health_revision_id: {type: string}
|
||||
health_source_at: {type: string}
|
||||
health_error: {type: string}
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/jobs/{job_id}/cancel:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
@@ -3904,6 +4081,33 @@ paths:
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/jobs/{job_id}/retry:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
post:
|
||||
tags: [Jobs]
|
||||
summary: Повторить завершившуюся ошибкой задачу
|
||||
description: Операторский повтор сохраняет ID и исходный запрос, сбрасывая бюджет попыток.
|
||||
operationId: postJobRetry
|
||||
responses:
|
||||
"202":
|
||||
description: Повтор поставлен в очередь.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Job"
|
||||
"409":
|
||||
$ref: "#/components/responses/Conflict"
|
||||
"429":
|
||||
description: Очередь задач заполнена.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Problem"
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/speakers/{speaker_id}/revisions/latest:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/SpeakerId"
|
||||
|
||||
Reference in new issue
Block a user