diff --git a/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md index 38ceac7..b38c64b 100644 --- a/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md +++ b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md @@ -1,6 +1,6 @@ --- name: EvoBGP архитектура -overview: 'Оба профиля по умолчанию на PostgreSQL (один движок, одни миграции). reference — микросервисы + PG + брокер; microVPS — evobgp-all + контейнер PG (жёсткий тюнинг). SQLite — опция только для одного контейнера без PG.' +overview: Оба профиля по умолчанию на PostgreSQL (один движок, одни миграции). reference — микросервисы + PG + брокер; microVPS — evobgp-all + контейнер PG (жёсткий тюнинг). SQLite — опция только для одного контейнера без PG. todos: - id: schema-db content: "Схема БД: PostgreSQL (основной); опционально SQLite (microVPS single-container); модули, ревизии, jobs" @@ -12,13 +12,13 @@ todos: content: REST (refresh, apply, preview, rollback, bundle API) и async jobs status: pending - id: node-agent - content: evobgp-agent на мастере; evobgp-node на реплике (отдельная ВМ) + content: evobgp-agent + bird2 в Docker на мастере; evobgp-node + тот же паттерн на реплике (отдельная ВМ) status: pending - id: observability content: Метрики, алерты префиксов и BGP-сессий status: pending - id: docker-ms - content: Compose profiles reference + microVPS, лимиты, логи, prune + content: Compose profiles reference + microVPS; сервисы bird2 + evobgp-agent; сеть BGP; лимиты; логи; prune status: pending - id: go-modules content: Monorepo internal/*, cmd/evobgp-all и cmd/* для reference @@ -40,6 +40,7 @@ Control-plane на **Go**, анонс префиксов через **BIRD**, п 1. [Два эталонных профиля](#1-два-эталонных-профиля-развёртывания) 2. [Логическая модель и термины](#2-логическая-модель-общая-для-обоих-профилей) 3. [Сервисы и контейнеры](#3-сервисы-сравнение-профилей) + - [3.1. Топология Docker Compose](#31-топология-docker-compose) 4. [База данных, ETL, ER-схема](#4-база-данных-etl-er-схема) 5. [Модули префиксов и FQDN](#5-модули-префиксов-as-cdn-домены) 6. [REST API](#6-rest-api) @@ -57,20 +58,20 @@ Control-plane на **Go**, анонс префиксов через **BIRD**, п Один и тот же **код** в дереве `internal/` (все подпакеты), две **упаковки** в Docker Compose. -| Критерий | **reference** (эталон) | **microVPS** | -| ---------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| **Назначение** | Production control-plane без жёсткого лимита RAM; горизонтальное масштабирование воркеров | Один хост: маленькая VPS / вложенная ВМ | -| **CPU** | 2+ vCPU (рекомендуется) | **1 vCPU** | -| **RAM** | 4+ ГиБ (ориентир) | **~1 ГиБ** | -| **Диск под Docker + данные** | по объёму проекта | **7–10 ГиБ** (бюджет; не весь диск ОС) | -| **ОС** | Linux (в т.ч. Ubuntu 22.04/24.04) | **Ubuntu 24.04 LTS** (референс) | -| **Контейнеры Go** | 5 образов: `api`, `scheduler`, `ingest`, `render`, `deploy` | **1** образ: `evobgp-all` | -| **БД** | **PostgreSQL** (отдельный сервис или managed) | **PostgreSQL** в контейнере (**жёсткий** тюнинг под 1 ГиБ) | -| **Очередь** | **NATS JetStream** или **Redis Streams** (на выбор) | **Нет брокера** — `job_audit` в **PostgreSQL** + `SKIP LOCKED` / малый пул коннектов | -| **Reverse proxy** | Traefik / Nginx (опционально) | **Нет** — прямой порт API | -| **Object storage** | MinIO / S3 опционально для артефактов | Только **локальный volume** или небольшие BLOB в БД | -| **BIRD** | Хост или privileged-контейнер + `evobgp-agent` | **Предпочтительно на хосте**; иначе +1 контейнер BIRD | -| **Контейнеры (итого)** | 5 Go + PG + брокер (+ опц. proxy) | **2:** `evobgp-all` + **postgres** (+ опц. BIRD-контейнер); без 5×Go и без брокера | +| Критерий | **reference** (эталон) | **microVPS** | +| ---------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| **Назначение** | Production control-plane без жёсткого лимита RAM; горизонтальное масштабирование воркеров | Один хост: маленькая VPS / вложенная ВМ | +| **CPU** | 2+ vCPU (рекомендуется) | **1 vCPU** | +| **RAM** | 4+ ГиБ (ориентир) | **~1 ГиБ** | +| **Диск под Docker + данные** | по объёму проекта | **7–10 ГиБ** (бюджет; не весь диск ОС) | +| **ОС** | Linux (в т.ч. Ubuntu 22.04/24.04) | **Ubuntu 24.04 LTS** (референс) | +| **Контейнеры Go** | 5 образов: `api`, `scheduler`, `ingest`, `render`, `deploy` | **1** образ: `evobgp-all` | +| **БД** | **PostgreSQL** (отдельный сервис или managed) | **PostgreSQL** в контейнере (**жёсткий** тюнинг под 1 ГиБ) | +| **Очередь** | **NATS JetStream** или **Redis Streams** (на выбор) | **Нет брокера** — `job_audit` в **PostgreSQL** + `SKIP LOCKED` / малый пул коннектов | +| **Reverse proxy** | Traefik / Nginx (опционально) | **Нет** — прямой порт API | +| **Object storage** | MinIO / S3 опционально для артефактов | Только **локальный volume** или небольшие BLOB в БД | +| **BIRD** | **BIRD 2** в **Docker** (privileged / нужные capabilities) + контейнер `**evobgp-agent`** с общим volume | То же: **bird2** + **evobgp-agent** + **postgres** + **evobgp-all** | +| **Контейнеры (итого)** | 5×Go + **postgres** + брокер + **bird2** + **evobgp-agent** (+ опц. proxy) | **4:** **evobgp-all** + **postgres** + **bird2** + **evobgp-agent** (+ опц. нода вне VPS) | ```mermaid @@ -79,13 +80,21 @@ flowchart LR R1[5x Go] R2[(PostgreSQL)] R3[(Broker)] + R4[bird2] + R5[evobgp-agent] R1 --> R2 R1 --> R3 + R1 --> R5 + R5 --> R4 end subgraph micro [microVPS] M1[evobgp-all] M2[(PostgreSQL)] + M3[bird2] + M4[evobgp-agent] M1 --> M2 + M1 --> M4 + M4 --> M3 end ``` @@ -101,28 +110,28 @@ flowchart LR - **BGP-сервер (ваш)** — процесс **BIRD**, который **анонсирует** префиксы и держит сессии. - **Клиенты** — **внешние** роутеры (BGP-пиры), подключающиеся **к вам** и **получающие** маршруты. Это не контейнеры EvoBGP. -- **Мастер** — control-plane + первичный BIRD (и `evobgp-agent` рядом с ним). -- **EvoBGP-нода (опционально)** — отдельная площадка: **только** pull подписанного **бандла** с мастера + локальный BIRD; **своей** полной БД и ingest **нет**. +- **Мастер** — control-plane + **BIRD 2** в **контейнере** и контейнер `**evobgp-agent`** (общий volume: сгенерированные `bird.d`, сокет/канал для `birdc`). +- **EvoBGP-нода (опционально)** — отдельная площадка: **только** pull подписанного **бандла** с мастера + **BIRD 2 в Docker** (тот же паттерн agent + bird2); **своей** полной БД и ingest **нет**. -**Быстрый путь данных:** БД → ingest (CDN / DoH / AS) → render (префиксы + community + ревизия) → deploy → файлы BIRD → `birdc configure` → клиентские сессии. +**Быстрый путь данных:** БД → ingest (CDN / DoH / AS; **IP_RANGES** только читаются при render из БД) → render (префиксы + community + ревизия) → deploy → файлы BIRD → `birdc configure` → клиентские сессии. --- ## 3. Сервисы (сравнение профилей) -| Сервис | Назначение | reference | microVPS | -| -------------------- | ---------------------------------------------- | ----------------------- | ------------------------ | -| **evobgp-api** | REST, CRUD, задачи, `/jobs` | отдельный контейнер | goroutine в `evobgp-all` | -| **evobgp-scheduler** | Интервалы модулей/CDN → события | отдельный контейнер | goroutine в `evobgp-all` | -| **evobgp-ingest** | CDN, DoH, материализация в БД | отдельный контейнер | goroutine в `evobgp-all` | -| **evobgp-render** | Итоговые префиксы, ревизия, текст BIRD | отдельный контейнер | goroutine в `evobgp-all` | -| **evobgp-deploy** | Доставка на мастерский BIRD, публикация бандла | отдельный контейнер | goroutine в `evobgp-all` | -| **evobgp-agent** | На хосте мастерского BIRD: файлы, `birdc` | бинарь/контейнер у BIRD | то же | -| **evobgp-node** | Реплика: pull бандла, локальный BIRD | отдельный хост | **не на microVPS** | +| Сервис | Назначение | reference | microVPS | +| -------------------- | --------------------------------------------------------------- | -------------------------------- | ------------------------ | +| **evobgp-api** | REST, CRUD, задачи, `/jobs` | отдельный контейнер | goroutine в `evobgp-all` | +| **evobgp-scheduler** | Интервалы модулей/CDN → события | отдельный контейнер | goroutine в `evobgp-all` | +| **evobgp-ingest** | CDN, DoH, материализация в БД | отдельный контейнер | goroutine в `evobgp-all` | +| **evobgp-render** | Итоговые префиксы, ревизия, текст BIRD | отдельный контейнер | goroutine в `evobgp-all` | +| **evobgp-deploy** | Доставка на мастерский BIRD, публикация бандла | отдельный контейнер | goroutine в `evobgp-all` | +| **evobgp-agent** | Контейнер рядом с **bird2**: запись конфигов, `birdc configure` | то же (общий volume с **bird2**) | то же | +| **evobgp-node** | Реплика: pull бандла, локальный BIRD | отдельный хост | **не на microVPS** | -**Инфраструктура reference:** PostgreSQL, NATS или Redis, опционально Traefik, MinIO. +**Инфраструктура reference:** PostgreSQL, NATS или Redis, опционально Traefik, MinIO, **bird2** + **evobgp-agent** в Docker. ### Зависимости (профиль reference) @@ -148,11 +157,69 @@ flowchart TB DEP --> DB DEP --> MQ DEP --> AG[evobgp-agent] - AG --> BIRD[BIRD мастер] + AG --> BIRD2[bird2] ``` +### 3.1. Топология Docker Compose + +Целевая упаковка: **все компоненты мастера в Compose**, включая **BIRD 2** (`bird2`). Пара **bird2 + evobgp-agent** делает **общий именованный volume** (или bind-mount) для каталога конфигурации и точки управления `birdc` (см. образ/entrypoint в репозитории). + +**Сеть для BGP:** на практике для входящих TCP **179** часто нужны `network_mode: host` (Linux), **macvlan/ipvlan** или публикация портов / отдельный L3-интерфейс — выбор фиксируется в профиле Compose и документации оператора; контейнер **bird2** получает **privileged** и набор **capabilities** (`NET_ADMIN`, и при необходимости `NET_RAW`), плюс `sysctls` под forwarding, если не на host-сети. + +```mermaid +flowchart TB + subgraph refDocker [profile_reference] + subgraph goRef [Сервисы Go] + API1[evobgp-api] + SCH1[scheduler] + ING1[ingest] + REN1[render] + DEP1[deploy] + end + PG1[(postgres)] + BR1[broker] + subgraph bgpRef [Стек BGP Docker] + AG1[evobgp-agent] + B21[bird2] + end + VOL1[vol_bird_config] + API1 --> PG1 + API1 --> BR1 + SCH1 --> PG1 + SCH1 --> BR1 + ING1 --> PG1 + ING1 --> BR1 + REN1 --> PG1 + REN1 --> BR1 + DEP1 --> PG1 + DEP1 --> BR1 + DEP1 --> AG1 + AG1 --> VOL1 + B21 --> VOL1 + AG1 -->|birdc| B21 + end + subgraph microDocker [profile_microVPS] + ALL[evobgp-all] + PG2[(postgres)] + subgraph bgpMicro [Стек BGP Docker] + AG2[evobgp-agent] + B22[bird2] + end + VOL2[vol_bird_config] + ALL --> PG2 + ALL --> AG2 + AG2 --> VOL2 + B22 --> VOL2 + AG2 -->|birdc| B22 + end +``` + + + +На **реплике** (`evobgp-node`) — тот же паттерн **bird2 + evobgp-agent** в Docker на отдельном хосте; pull бандла и `birdc configure` без полной БД (см. §9). + --- ## 4. База данных, ETL, ER-схема @@ -160,17 +227,17 @@ flowchart TB ### 4.1. Группы сущностей -| Область | Назначение | -| ------------------------------------------------------ | ----------------------------------------------------------------------- | -| **module** | Тип `AS_PREFIXES` / `CDN_CIDRS` / `DOMAINS`, расписание, DoH, приоритет | -| **module_cdn_source** | URL/inline CDN; свой `refresh_interval` опционально | -| **doh_profile** | URL DoH, таймауты, секреты по ссылке | -| **module_domain_entry** / **module_as_entry** | FQDN или ASN/префикс + `community_id` | -| **bgp_community** | Справочник community для BIRD | -| **bgp_peer** | Клиентский пир; `speaker_id` NULL = все спикеры в бандле | -| **bgp_speaker** | master / replica, `last_applied_revision_id` | -| **config_revision** / **revision_materialized_prefix** | История, diff, откат | -| **job_audit** | Асинхронные задачи; в reference дополняет брокер | +| Область | Назначение | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **module** | Тип `AS_PREFIXES` / `CDN_CIDRS` / `DOMAINS` / `IP_RANGES`, расписание, DoH, приоритет | +| **module_cdn_source** | URL + `source_kind` (формат тела списка); примеры наброска: `text_cidr_lines`, `json_prefix_list`, `custom`; ETag; свой `refresh_interval` опционально | +| **doh_profile** | URL DoH, таймауты, секреты по ссылке | +| **module_domain_entry** / **module_as_entry** / **module_ip_range_entry** | FQDN; или ASN/префикс; или **только CIDR/диапазон** + `community_id` (`IP_RANGES`) | +| **bgp_community** | Справочник community для BIRD | +| **bgp_peer** | Клиентский пир; `speaker_id` NULL = все спикеры в бандле | +| **bgp_speaker** | master / replica, `last_applied_revision_id` | +| **config_revision** / **revision_materialized_prefix** | История, diff, откат | +| **job_audit** | Асинхронные задачи; в reference дополняет брокер | ### 4.2. ETL @@ -181,6 +248,7 @@ flowchart LR CDN[CDN fetch] DOH[DoH] AS[AS из БД] + IPR[IP ranges из БД] end subgraph tr [Transform] NORM[Нормализация CIDR] @@ -195,6 +263,7 @@ flowchart LR CDN --> NORM DOH --> NORM AS --> NORM + IPR --> NORM NORM --> DEDUP --> COMM COMM --> DB COMM --> REV @@ -215,8 +284,10 @@ erDiagram module ||--o{ module_cdn_source : contains module ||--o{ module_domain_entry : contains module ||--o{ module_as_entry : contains + module ||--o{ module_ip_range_entry : contains bgp_community ||--o{ module_domain_entry : tags bgp_community ||--o{ module_as_entry : tags + bgp_community ||--o{ module_ip_range_entry : tags bgp_community ||--o{ module_cdn_source : tags bgp_speaker ||--o{ bgp_peer : scope module ||--o{ config_revision : produces @@ -229,20 +300,21 @@ erDiagram ### 4.4. Пояснения к таблицам -| Таблица | Назначение | Ключевые поля | Кто использует | -| -------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------ | -| **tenant** | Multi-tenant | `id`, `name`, `slug` | API, все сервисы | -| **module** | Блок политики | `type`, `enabled`, `priority`, `doh_profile_id`, `refresh_interval_sec`, `cron_expr`, `default_community_id` | API, scheduler, ingest, render | -| **doh_profile** | DoH | `url`, таймауты, ссылка на секрет | DOMAINS, ingest | -| **module_cdn_source** | Строка CDN | `source_kind`, `url`, `etag`, `refresh_interval_sec`, `community_id` | ingest, render | -| **module_domain_entry** | FQDN | `fqdn`, `community_id`, метаданные резолва | ingest, render | -| **module_as_entry** | AS/префикс | `asn`, `prefix`, `community_id` | API, render | -| **bgp_community** | Справочник | `kind`, значения, уникальность в tenant | API, render | -| **bgp_peer** | Клиентский пир | neighbor, ASN, политики, `bgp_speaker_id` (NULL = все спикеры) | API, render, бандл | -| **bgp_speaker** | Экземпляр BIRD | `role` master/replica, endpoint, `last_applied_revision_id` | deploy, нода | -| **config_revision** | История | `hash`, артефакт, `parent_revision_id` | render, deploy, rollback | -| **revision_materialized_prefix** | Снимок префиксов | `revision_id`, `prefix`, `community_id`, `source` | preview, diff, откат | -| **job_audit** | Задачи | `kind`, `status`, `idempotency_key` | API, воркеры | +| Таблица | Назначение | Ключевые поля | Кто использует | +| -------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | +| **tenant** | Multi-tenant | `id`, `name`, `slug` | API, все сервисы | +| **module** | Блок политики | `type`, `enabled`, `priority`, `doh_profile_id`, `refresh_interval_sec`, `cron_expr`, `default_community_id` | API, scheduler, ingest, render | +| **doh_profile** | DoH | `url`, таймауты, ссылка на секрет | DOMAINS, ingest | +| **module_cdn_source** | Строка CDN | `source_kind` (набросок: `text_cidr_lines`, `json_prefix_list`, `custom`), `url`, `etag`, `refresh_interval_sec`, `community_id` | ingest, render | +| **module_domain_entry** | FQDN | `fqdn`, `community_id`, метаданные резолва | ingest, render | +| **module_as_entry** | AS/префикс | `asn`, `prefix`, `community_id` | API, render | +| **module_ip_range_entry** | Статический CIDR | `prefix` (CIDR), `community_id`; без ASN и без внешнего URL — модуль типа `IP_RANGES` | API, render | +| **bgp_community** | Справочник | `kind`, значения, уникальность в tenant | API, render | +| **bgp_peer** | Клиентский пир | neighbor, ASN, политики, `bgp_speaker_id` (NULL = все спикеры) | API, render, бандл | +| **bgp_speaker** | Экземпляр BIRD | `role` master/replica, endpoint, `last_applied_revision_id` | deploy, нода | +| **config_revision** | История | `hash`, артефакт, `parent_revision_id` | render, deploy, rollback | +| **revision_materialized_prefix** | Снимок префиксов | `revision_id`, `prefix`, `community_id`, `source` | preview, diff, откат | +| **job_audit** | Задачи | `kind`, `status`, `idempotency_key` | API, воркеры | Дополнительно: `**global_settings`** (KV); `**module_cdn_fetch_log`** (опционально, TTL). @@ -253,12 +325,17 @@ erDiagram ## 5. Модули префиксов (AS / CDN / домены) +### Продуктовая трактовка -| Тип | Ввод | Поведение | -| ---------- | -------------- | -------------------------------------------------------- | -| **AS** | ASN / префиксы | Таблица + опционально внешний `PrefixProvider` | -| **CDN** | URL или список | Fetch, ETag, интервал модуля или строки | -| **Домены** | FQDN | В BIRD попадают **только** IP-префиксы после DoH-резолва | +В терминах продукта **модуль** — это один из **четырёх видов источника префиксов**: **AS** (`AS_PREFIXES`), **CDN** (`CDN_CIDRS`), **DOMAINS** (`DOMAINS`), **IP-диапазоны** (`IP_RANGES`). Наполнение задаётся либо **записями в таблице** (ASN или префикс + `community_id`; FQDN + `community_id` для доменов; **CIDR + `community_id`** для `IP_RANGES` без ASN и без внешнего URL), либо для CDN — **источником по URL** с полем `source_kind`, определяющим формат скачанного списка и парсер. Строка сущности `**module`** в БД — это экземпляр модуля выбранного типа (расписание, приоритет, DoH для доменов и т.д.); `**module_id` в API** — идентификатор экземпляра, а не «имя типа». При минимальном развёртывании (один bird2, мало пиров) допустим **один экземпляр на каждый нужный тип** или узкий набор экземпляров — это не противоречит модели. + + +| Тип | Ввод | Поведение | +| ---------------- | ---------------------------- | ------------------------------------------------------------------------- | +| **AS** | ASN / префиксы | Таблица + опционально внешний `PrefixProvider` | +| **CDN** | URL или список | Fetch, ETag, интервал модуля или строки | +| **Домены** | FQDN | В BIRD попадают **только** IP-префиксы после DoH-резолва | +| **IP-диапазоны** | CIDR (IPv4/IPv6) + community | Только таблица `module_ip_range_entry`; без IRR/ASN-семантики и без fetch | **FQDN:** воркер резолвит через **DoH-профиль** модуля → `/32` / `/128` (или политика) → материализация в БД → генерация static include для BIRD. @@ -267,6 +344,8 @@ erDiagram ## 6. REST API +Наброски путей, ролей и контрактов вынесены в **[docs/evobgp-api-sketches.md](docs/evobgp-api-sketches.md)** (`/v1`, задачи, бандлы нод). + - `POST /modules/{id}/refresh` - `POST /apply`, `POST /speakers/{id}/apply` или `/bird/apply` - `GET /revisions`, `POST /revisions/{id}/rollback` @@ -291,7 +370,7 @@ erDiagram - Canary: несколько `bgp_speaker` или подмножество пиров по фильтру. - Метрики: размер префикс-сета, ошибки DoH/CDN, `birdc show protocols`. - Секреты BGP — Vault / K8s secrets, не plaintext в БД. -- Last-known-good конфиг на хосте BIRD. +- Last-known-good конфиг на **volume** / bind-mount, общий для **bird2** и **evobgp-agent** (или снимок на хосте при bind-mount). --- @@ -325,7 +404,7 @@ erDiagram - нормальные **advisory locks** / `SKIP LOCKED` для очереди `job_audit`; - проще подключить **внешний** managed Postgres при росте. -На **microVPS** отдельный контейнер `postgres` — **осознанная плата** за единообразие (2 контейнера: `evobgp-all` + `postgres`). +На **microVPS** отдельные контейнеры `**postgres`** и стек **bird2 + evobgp-agent** — **осознанная плата** за единообразие с **reference** (минимум: `evobgp-all` + `postgres` + `bird2` + `evobgp-agent`). ### PostgreSQL: память и CPU (ориентиры) @@ -352,11 +431,11 @@ Raft, HTTP/`gorqlite`; для HA control-plane; не замена дефолтн ### Контейнеры -| Вариант | Состав | -| --------------- | ----------------------------------------------------------------------------- | -| **A (целевой)** | **evobgp-all** + **postgres** (официальный образ Postgres, volume для данных) | -| **B** | A + контейнер **BIRD** (privileged), если BIRD не на хосте Ubuntu | -| **C (опция)** | Только **evobgp-all** + SQLite на volume — профиль **microVPS_sqlite** | +| Вариант | Состав | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| **A (целевой)** | **evobgp-all** + **postgres** + **bird2** + **evobgp-agent** (общий volume конфигов BIRD; см. [§3.1](#31-топология-docker-compose)) | +| **B (опция)** | Без отдельного PG — профиль **microVPS_sqlite** + тот же стек **bird2** + **evobgp-agent** | +| **C (legacy)** | BIRD только на хосте ОС — **не целевой путь**, только для отладки или жёстких ограничений Docker | ### Бюджет диска 7–10 ГиБ @@ -376,8 +455,8 @@ Raft, HTTP/`gorqlite`; для HA control-plane; не замена дефолтн ### CPU и RAM - Worker pool ingest/render: **1–2**; без агрессивного параллелизма на одном ядре. -- `deploy.resources`: лимиты на контейнеры **evobgp-all** и **postgres** (например PG `mem_limit` **256m–384m**, приложение **384m–512m** — подобрать по `free -m` на хосте). -- Ориентир суммарно: **~350–650 МиБ** простой, **~450–800 МиБ** пик (**PG + Go + система + BIRD**) — на **1 ГиБ** нужны swap или ещё более жёсткий `shared_buffers`. +- `deploy.resources`: лимиты на **evobgp-all**, **postgres**, **bird2**, **evobgp-agent** (на PG ориентир **256m–384m**, на пару **bird2+agent** заложить **128m–256m** — уточнить по `docker stats`). +- Ориентир суммарно: **~400–750 МиБ** простой, **~550–900 МиБ** пик (**PG + Go + bird2 + система**) — на **1 ГиБ** обычно нужны **swap** и жёсткий тюнинг PG/`bird2`. ### Tuning PostgreSQL (microVPS) diff --git a/docs/evobgp-api-sketches.md b/docs/evobgp-api-sketches.md new file mode 100644 index 0000000..86c8496 --- /dev/null +++ b/docs/evobgp-api-sketches.md @@ -0,0 +1,270 @@ +# EvoBGP — наброски HTTP API + +**Статус:** черновик для согласования; не спецификация реализации. Базовый префикс: **`/v1`**. Модель данных и термины — в архитектурном плане (модули, `bgp_speaker`, `config_revision`, `job_audit`). + +--- + +## 1. Общие соглашения + +| Тема | Решение (набросок) | +|------|---------------------| +| **Аутентификация** | Заголовок `Authorization: Bearer ` или mTLS на edge; ключи привязаны к tenant и роли. | +| **Multi-tenant** | Все сущности в скоупе tenant: либо из ключа, либо явный префикс `X-Tenant-Id` (только для супер-ролей). | +| **Идентификаторы** | UUID v7 или ULID в URL; в JSON — строки. | +| **Время** | ISO 8601 UTC (`2026-04-03T12:00:00Z`). | +| **Ошибки** | Тело `application/problem+json` (RFC 9457): `type`, `title`, `status`, `detail`, `instance`, опционально `errors[]` по полям. | +| **Идемпотентность** | Для мутаций, создающих задачи или побочные эффекты: заголовок `Idempotency-Key` (опционально обязателен для `POST` apply/refresh). | +| **Пагинация** | `?cursor=&limit=50` (cursor-based); ответ: `items`, `next_cursor`, `has_more`. | +| **Асинхронные операции** | `202 Accepted`, заголовок `Location: /v1/jobs/{job_id}`; тело `{ "job_id", "status": "queued" }`. | +| **Версионирование** | Несовместимые изменения — новый префикс `/v2`. | + +--- + +## 2. Системные и служебные + +| Метод | Путь | Назначение | +|-------|------|------------| +| `GET` | `/v1/health` | Liveness (процесс жив). | +| `GET` | `/v1/ready` | Readiness (БД, брокер при reference, и т.д.). | +| `GET` | `/v1/version` | Версия сборки API и control-plane (`git_sha`, `build_time`). | + +--- + +## 3. Модули префиксов (`module`) + +### Связь с продуктом + +**ASN / CDN / DOMAINS / IP-диапазоны** в продуктовой формулировке — это **четыре вида модулей**. Поле **`type`** в `POST /v1/modules` выбирает вид: `AS_PREFIXES`, `CDN_CIDRS`, `DOMAINS` или **`IP_RANGES`**. Дочерние ресурсы: **`as-entries`**, **`cdn-sources`**, **`domain-entries`** — как раньше; **`ip-range-entries`** — статические **CIDR + `community_id`** (без ASN и без URL). **`module_id`** в пути — идентификатор **конкретного экземпляра** модуля, а не имя типа. + +Типы: `AS_PREFIXES`, `CDN_CIDRS`, `DOMAINS`, `IP_RANGES` (как в плане). + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/modules` | Список модулей tenant (фильтры: `?type=`, `?enabled=`). | +| `POST` | `/v1/modules` | Создать модуль. | +| `GET` | `/v1/modules/{module_id}` | Детали модуля. | +| `PATCH` | `/v1/modules/{module_id}` | Частичное обновление (расписание, DoH, приоритет, `enabled`). | +| `DELETE` | `/v1/modules/{module_id}` | Мягкое удаление или `enabled=false` — зафиксировать в плане реализации. | + +**CDN-источники модуля** + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/modules/{module_id}/cdn-sources` | Список строк CDN. | +| `POST` | `/v1/modules/{module_id}/cdn-sources` | Добавить источник (URL + `source_kind` + опционально `community_id`). | +| `PATCH` | `/v1/modules/{module_id}/cdn-sources/{source_id}` | URL, `source_kind`, `community_id`, свой `refresh_interval_sec`. | +| `DELETE` | `/v1/modules/{module_id}/cdn-sources/{source_id}` | Удалить. | + +**Записи AS / домены** + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/modules/{module_id}/as-entries` | Список ASN/префиксов. | +| `POST` | `/v1/modules/{module_id}/as-entries` | Добавить. | +| `PATCH` | `/v1/modules/{module_id}/as-entries/{entry_id}` | Обновить. | +| `DELETE` | `/v1/modules/{module_id}/as-entries/{entry_id}` | Удалить. | +| `GET` | `/v1/modules/{module_id}/domain-entries` | FQDN + community. | +| `POST` | `/v1/modules/{module_id}/domain-entries` | Добавить. | +| `PATCH` | `/v1/modules/{module_id}/domain-entries/{entry_id}` | Обновить. | +| `DELETE` | `/v1/modules/{module_id}/domain-entries/{entry_id}` | Удалить. | + +**Записи IP-диапазонов** (только для `type: IP_RANGES`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/modules/{module_id}/ip-range-entries` | Список CIDR + `community_id`. | +| `POST` | `/v1/modules/{module_id}/ip-range-entries` | Добавить запись (`prefix`, `community_id`). | +| `PATCH` | `/v1/modules/{module_id}/ip-range-entries/{entry_id}` | Обновить. | +| `DELETE` | `/v1/modules/{module_id}/ip-range-entries/{entry_id}` | Удалить. | + +**Refresh (ingest)** + +| Метод | Путь | Описание | +|-------|------|----------| +| `POST` | `/v1/modules/{module_id}/refresh` | Запуск ingest для модуля (CDN / DoH / AS по типу). Для **`IP_RANGES`** обычно **не требуется** (данные только в БД); возможен **`204`** / no-op или отказ **`400`**, если тип не поддерживает refresh — зафиксировать в реализации. | + +**Пример тела создания модуля (набросок)** + +```json +{ + "type": "CDN_CIDRS", + "name": "edge-v4", + "enabled": true, + "priority": 10, + "doh_profile_id": null, + "refresh_interval_sec": 3600, + "cron_expr": null, + "default_community_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +Пример модуля **`IP_RANGES`** (набросок): `type: "IP_RANGES"`, `doh_profile_id: null`, далее строки через `ip-range-entries` с полями `prefix` (например `203.0.113.0/24`) и `community_id`. + +--- + +## 4. DoH-профили (`doh_profile`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/doh-profiles` | Список. | +| `POST` | `/v1/doh-profiles` | Создать (URL, таймауты; секрет — ссылка на vault id или отдельный `POST .../secret`). | +| `GET` | `/v1/doh-profiles/{id}` | Детали (без раскрытия секрета). | +| `PATCH` | `/v1/doh-profiles/{id}` | Обновить. | +| `DELETE` | `/v1/doh-profiles/{id}` | Удалить, если не используется модулями. | + +--- + +## 5. BGP community (`bgp_community`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/communities` | Список справочника. | +| `POST` | `/v1/communities` | Создать. | +| `GET` | `/v1/communities/{id}` | Детали. | +| `PATCH` | `/v1/communities/{id}` | Обновить. | +| `DELETE` | `/v1/communities/{id}` | Удалить при отсутствии ссылок. | + +--- + +## 6. Пиры (`bgp_peer`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/peers` | Список (`?speaker_id=`, пагинация). | +| `POST` | `/v1/peers` | Создать пира. | +| `GET` | `/v1/peers/{id}` | Детали. | +| `PATCH` | `/v1/peers/{id}` | Политики, neighbor, ASN, привязка к `bgp_speaker_id` или `null` = все спикеры. | +| `DELETE` | `/v1/peers/{id}` | Удалить / отключить. | + +--- + +## 7. Спикеры BIRD (`bgp_speaker`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/speakers` | Список (master / replica, endpoint). | +| `POST` | `/v1/speakers` | Зарегистрировать спикер (реплика, canary). | +| `GET` | `/v1/speakers/{id}` | Детали + `last_applied_revision_id`. | +| `PATCH` | `/v1/speakers/{id}` | Метаданные, endpoint. | + +--- + +## 8. Ревизии конфигурации (`config_revision`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/revisions` | История ревизий (`?module_id=`, `?limit=`). | +| `GET` | `/v1/revisions/{revision_id}` | Метаданные: хэш, родитель, время, артефакты. | +| `GET` | `/v1/revisions/{revision_id}/prefixes` | Материализованный снимок префиксов (пагинация). | +| `GET` | `/v1/revisions/{revision_id}/preview` | Превью фрагментов BIRD (read-only, без apply). | +| `POST` | `/v1/revisions/{revision_id}/rollback` | Создать **новую** ревизию с содержимым отката; часто **`202`**. | + +**Сравнение ревизий (набросок)** + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/revisions/{a}/diff/{b}` | Diff префиксов / метаданных (формат — зафиксировать: JSON patch или табличный). | + +--- + +## 9. Применение конфигурации (deploy / BIRD) + +| Метод | Путь | Описание | +|-------|------|----------| +| `POST` | `/v1/apply` | Применить текущую целевую ревизию на всех спикерах (или по политике по умолчанию). **`202`**. | +| `POST` | `/v1/speakers/{id}/apply` | Применить на одном спикере (canary). **`202`**. | +| `POST` | `/v1/bird/reload` | Опционально: явный мягкий reload политики (если отделён от apply); иначе часть `apply`. | + +**Тело `POST /v1/apply` (набросок, опционально)** + +```json +{ + "revision_id": "01JQXYZ...", + "strategy": "all_speakers", + "dry_run": false +} +``` + +--- + +## 10. Задачи (`job_audit`) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/jobs` | Список задач (`?status=`, `?kind=`, cursor). | +| `GET` | `/v1/jobs/{job_id}` | Статус, прогресс, ошибка, связанные сущности. | +| `POST` | `/v1/jobs/{job_id}/cancel` | Запрос отмены (best-effort). | + +**Пример ответа `GET /v1/jobs/{id}`** + +```json +{ + "job_id": "01JQXYZ...", + "kind": "module_refresh", + "status": "running", + "idempotency_key": "client-abc-123", + "created_at": "2026-04-03T10:00:00Z", + "started_at": "2026-04-03T10:00:01Z", + "finished_at": null, + "error": null, + "meta": { "module_id": "01JQM..." } +} +``` + +--- + +## 11. Реплики (`evobgp-node`): бандлы + +Вызываются **нодой** с отдельным ключом / mTLS (роль `node`). + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/speakers/{speaker_id}/revisions/latest` | Указатель на последнюю опубликованную ревизию для ноды. | +| `GET` | `/v1/speakers/{speaker_id}/bundle/{revision_id}` | Скачивание подписанного бандла (архив + `manifest.json` + подпись). | +| `POST` | `/v1/nodes/enroll` | Регистрация ноды (обмен ключами, привязка к `speaker_id`) — детали протокола отдельно. | + +Заголовки для бандла: `Content-Type: application/octet-stream` или multipart; контроль целостности по `manifest` (SHA-256) и подписи (например Ed25519). + +--- + +## 12. Глобальные настройки и операторские флаги (опционально) + +| Метод | Путь | Описание | +|-------|------|----------| +| `GET` | `/v1/settings` | KV вроде `global_settings` (лимиты CDN, feature flags). | +| `PATCH` | `/v1/settings` | Частичное обновление (только роль operator). | + +--- + +## 13. Набросок матрицы прав (роли) + +| Ресурс | `viewer` | `editor` | `operator` | `node` | +|--------|----------|----------|------------|--------| +| GET модули, ревизии, peers | да | да | да | нет* | +| PATCH модули, peers | нет | да | да | нет | +| apply, rollback | нет | нет | да | нет | +| bundle / enroll | нет | нет | нет | да | + +\*Нода не ходит в общий CRUD; только §11. + +--- + +## 14. Что вынести в следующую итерацию + +- Полная **OpenAPI 3.1** схема (`openapi.yaml`) из этого документа. +- Webhooks: `POST` на URL клиента по завершении `job` (опционально). +- SSE/WebSocket для стрима статуса долгих jobs. +- Rate limits по ключу и по tenant в ответах (`RateLimit-*` заголовки). + +--- + +## 15. Связь с архитектурным планом + +| Тема плана | Раздел здесь | +|------------|----------------| +| REST, jobs | §1, §10 | +| refresh, apply, rollback, preview, `IP_RANGES` | §3, §8, §9 | +| peers, speakers, communities, DoH | §4–§7 | +| bundle API, нода | §11 | + +Файл плана: `.cursor/plans/evobgp_архитектура_0e73ef02.plan.md` (§6 REST API). diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..58fd4ce --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,2313 @@ +openapi: 3.1.0 +info: + title: EvoBGP Control Plane API + version: 0.1.0 + description: | + REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs). + + **Соглашения:** префикс `/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`. + + **Роли** (матрица доступа): `viewer`, `editor`, `operator`, `node`. Нода использует отдельные пути и ключ с ролью `node`. + + Заголовок `X-Tenant-Id` допускается только для супер-ролей (явный tenant); иначе tenant берётся из API-ключа. + license: + name: Proprietary + identifier: LicenseRef-Proprietary + +servers: + - url: https://api.example.com + description: Базовый URL инсталляции (замените на свой) + +tags: + - name: System + description: Liveness, readiness и метаданные сборки. Обычно без чувствительных данных; доступ может быть шире. + - name: Modules + description: Экземпляры модулей префиксов (AS, CDN, домены, статические IP-диапазоны) и вложенные записи. Чтение — viewer+; изменение — editor+. + - name: DoH profiles + description: Профили DNS-over-HTTPS для модулей типа домены. Секрет в ответах не возвращается. + - name: Communities + description: Справочник BGP community в скоупе tenant. + - name: Peers + description: BGP-пиры и политики; привязка к спикеру или ко всем спикерам. + - name: Speakers + description: Регистрация и метаданные экземпляров BIRD (master/replica/canary). + - name: Revisions + description: История ревизий конфигурации, снимки префиксов, превью и откат (часто асинхронно). + - name: Deploy + description: Применение ревизий на спикерах и опциональный reload BIRD. Требуется роль operator. + - name: Jobs + description: Асинхронные задачи; статус и отмена (best-effort). + - name: Node + description: "API для evobgp-node (бандлы ревизий и enrollment). Отдельный ключ или mTLS, роль node." + - name: Settings + description: Глобальные настройки и feature flags; изменение — только operator. + +security: + - bearerAuth: [] + +externalDocs: + description: Черновик и контекст API + url: ./evobgp-api-sketches.md + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: API-ключ в формате `Bearer `. На edge может заменяться или дополняться mTLS. + + parameters: + TenantId: + name: X-Tenant-Id + in: header + required: false + schema: + $ref: "#/components/schemas/ResourceId" + description: Явный tenant (только супер-роли). Без заголовка tenant определяется по ключу. + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + schema: + type: string + maxLength: 256 + description: Идемпотентность мутаций. Для `POST` apply и части refresh рекомендуется или обязателен по политике продукта. + Cursor: + name: cursor + in: query + schema: + type: string + description: Непрозрачный курсор из предыдущего ответа. + Limit: + name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 500 + default: 50 + description: Размер страницы (по умолчанию 50). + ModuleId: + name: module_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + SpeakerId: + name: speaker_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + RevisionId: + name: revision_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + JobId: + name: job_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + PeerId: + name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + DohProfileId: + name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + CommunityId: + name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + SourceId: + name: source_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + EntryId: + name: entry_id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + RevisionA: + name: revision_a + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + RevisionB: + name: revision_b + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + ModuleTypeFilter: + name: type + in: query + schema: + $ref: "#/components/schemas/ModuleType" + ModuleEnabledFilter: + name: enabled + in: query + schema: + type: boolean + SpeakerFilter: + name: speaker_id + in: query + schema: + $ref: "#/components/schemas/ResourceId" + RevisionModuleFilter: + name: module_id + in: query + schema: + $ref: "#/components/schemas/ResourceId" + JobStatusFilter: + name: status + in: query + schema: + $ref: "#/components/schemas/JobStatus" + JobKindFilter: + name: kind + in: query + schema: + type: string + description: Фильтр по виду задачи; точный перечень расширяем. + + responses: + Unauthorized: + description: Нет или невалидный Bearer-токен. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + Forbidden: + description: Недостаточно прав для операции. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + NotFound: + description: Ресурс не найден в скоупе tenant. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + Conflict: + description: Конфликт состояния (например, удаление DoH-профиля, который ещё используется). + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + UnprocessableEntity: + description: Семантическая ошибка запроса или валидации полей. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + DefaultProblem: + description: Ошибка (см. тело Problem). + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + + schemas: + ResourceId: + type: string + description: UUID v7 или ULID. + pattern: "^[0-9A-Za-z_-]{20,36}$" + examples: + - 01JQXYZABCDEFGHIJKLMNOPQRS + + Problem: + type: object + description: Problem Details (RFC 9457). + required: + - title + - status + properties: + type: + type: string + format: uri-reference + description: URI типа проблемы. + title: + type: string + status: + type: integer + detail: + type: string + instance: + type: string + format: uri-reference + errors: + type: array + items: + $ref: "#/components/schemas/FieldError" + additionalProperties: true + + FieldError: + type: object + properties: + field: + type: string + description: Имя поля (или JSON Pointer). + pointer: + type: string + description: JSON Pointer к значению. + message: + type: string + additionalProperties: true + + AsyncJobAccepted: + type: object + required: + - job_id + - status + properties: + job_id: + $ref: "#/components/schemas/ResourceId" + status: + type: string + enum: [queued] + description: Начальный статус задачи в очереди. + + ModuleType: + type: string + enum: + - AS_PREFIXES + - CDN_CIDRS + - DOMAINS + - IP_RANGES + + JobStatus: + type: string + description: Статус задачи; перечень может расширяться. + enum: + - queued + - running + - succeeded + - failed + - cancelled + + ApplyStrategy: + type: string + description: Стратегия выбора спикеров для apply. + enum: + - all_speakers + + Module: + type: object + required: + - id + - type + - name + - enabled + - priority + properties: + id: + $ref: "#/components/schemas/ResourceId" + type: + $ref: "#/components/schemas/ModuleType" + name: + type: string + enabled: + type: boolean + priority: + type: integer + doh_profile_id: + type: ["string", "null"] + refresh_interval_sec: + type: ["integer", "null"] + minimum: 0 + cron_expr: + type: ["string", "null"] + default_community_id: + type: ["string", "null"] + additionalProperties: true + + ModuleCreate: + type: object + required: + - type + - name + properties: + type: + $ref: "#/components/schemas/ModuleType" + name: + type: string + enabled: + type: boolean + default: true + priority: + type: integer + default: 0 + doh_profile_id: + type: ["string", "null"] + refresh_interval_sec: + type: ["integer", "null"] + cron_expr: + type: ["string", "null"] + default_community_id: + type: ["string", "null"] + + ModulePatch: + type: object + properties: + name: + type: string + enabled: + type: boolean + priority: + type: integer + doh_profile_id: + type: ["string", "null"] + refresh_interval_sec: + type: ["integer", "null"] + cron_expr: + type: ["string", "null"] + default_community_id: + type: ["string", "null"] + + CdnSource: + type: object + required: + - id + - url + - source_kind + properties: + id: + $ref: "#/components/schemas/ResourceId" + url: + type: string + format: uri + source_kind: + type: string + description: Формат скачанного списка / парсер. + community_id: + type: ["string", "null"] + refresh_interval_sec: + type: ["integer", "null"] + + CdnSourceCreate: + type: object + required: + - url + - source_kind + properties: + url: + type: string + format: uri + source_kind: + type: string + community_id: + type: ["string", "null"] + + CdnSourcePatch: + type: object + properties: + url: + type: string + format: uri + source_kind: + type: string + community_id: + type: ["string", "null"] + refresh_interval_sec: + type: ["integer", "null"] + + AsEntry: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + asn: + type: ["integer", "null"] + prefix: + type: ["string", "null"] + description: CIDR или префикс в зависимости от модели. + community_id: + type: ["string", "null"] + additionalProperties: true + + AsEntryCreate: + type: object + properties: + asn: + type: integer + prefix: + type: string + community_id: + type: ["string", "null"] + additionalProperties: true + + DomainEntry: + type: object + required: + - id + - fqdn + properties: + id: + $ref: "#/components/schemas/ResourceId" + fqdn: + type: string + community_id: + type: ["string", "null"] + + DomainEntryCreate: + type: object + required: + - fqdn + properties: + fqdn: + type: string + community_id: + type: ["string", "null"] + + IpRangeEntry: + type: object + required: + - id + - prefix + - community_id + properties: + id: + $ref: "#/components/schemas/ResourceId" + prefix: + type: string + description: IPv4/IPv6 CIDR. + example: 203.0.113.0/24 + community_id: + $ref: "#/components/schemas/ResourceId" + + IpRangeEntryCreate: + type: object + required: + - prefix + - community_id + properties: + prefix: + type: string + community_id: + $ref: "#/components/schemas/ResourceId" + + DohProfile: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + url: + type: string + format: uri + timeout_ms: + type: integer + vault_secret_ref: + type: ["string", "null"] + description: Ссылка на секрет в vault; сырое значение не отдаётся в GET. + additionalProperties: true + + DohProfileCreate: + type: object + required: + - url + properties: + url: + type: string + format: uri + timeout_ms: + type: integer + vault_secret_ref: + type: ["string", "null"] + + BgpCommunity: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + name: + type: string + kind: + type: string + additionalProperties: true + + BgpPeer: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + neighbor: + type: string + description: IP соседа. + remote_asn: + type: integer + bgp_speaker_id: + type: ["string", "null"] + description: "`null` — политика для всех спикеров." + additionalProperties: true + + BgpSpeaker: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + role: + type: string + description: master, replica, canary и т.д. + endpoint: + type: string + last_applied_revision_id: + type: ["string", "null"] + additionalProperties: true + + ConfigRevision: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/ResourceId" + parent_revision_id: + type: ["string", "null"] + content_hash: + type: string + created_at: + type: string + format: date-time + additionalProperties: true + + PrefixSnapshotItem: + type: object + description: Элемент материализованного снимка префиксов (детали — по реализации). + additionalProperties: true + + Job: + type: object + required: + - job_id + - kind + - status + - created_at + properties: + job_id: + $ref: "#/components/schemas/ResourceId" + kind: + type: string + status: + $ref: "#/components/schemas/JobStatus" + idempotency_key: + type: ["string", "null"] + created_at: + type: string + format: date-time + started_at: + type: ["string", "null"] + description: ISO 8601 UTC, если задача уже стартовала. + finished_at: + type: ["string", "null"] + description: ISO 8601 UTC по завершении. + error: + type: ["string", "null"] + meta: + type: object + additionalProperties: true + + ApplyRequest: + type: object + properties: + revision_id: + $ref: "#/components/schemas/ResourceId" + strategy: + $ref: "#/components/schemas/ApplyStrategy" + dry_run: + type: boolean + default: false + + VersionInfo: + type: object + properties: + api_version: + type: string + git_sha: + type: string + build_time: + type: string + format: date-time + additionalProperties: true + + HealthStatus: + type: object + properties: + status: + type: string + example: ok + additionalProperties: true + + ReadyStatus: + type: object + properties: + status: + type: string + example: ready + checks: + type: object + additionalProperties: true + additionalProperties: true + + Settings: + type: object + description: KV настройки tenant/глобальные лимиты и feature flags. + additionalProperties: true + + RevisionDiff: + type: object + description: | + Сравнение двух ревизий. Конкретный формат (JSON Patch, табличный diff и т.д.) задаётся реализацией — контракт может уточняться. + additionalProperties: true + + NodeEnrollRequest: + type: object + description: Тело регистрации ноды; протокол обмена ключами уточняется отдельно. + properties: + public_key: + type: string + speaker_id: + $ref: "#/components/schemas/ResourceId" + additionalProperties: true + + NodeEnrollResponse: + type: object + description: Ответ enrollment (плейсхолдер). + additionalProperties: true + + DohProfilePatch: + type: object + properties: + url: + type: string + format: uri + timeout_ms: + type: integer + vault_secret_ref: + type: ["string", "null"] + + BgpCommunityCreate: + type: object + required: [name] + properties: + name: + type: string + kind: + type: string + additionalProperties: true + + BgpCommunityPatch: + type: object + properties: + name: + type: string + kind: + type: string + additionalProperties: true + + BgpPeerCreate: + type: object + required: [neighbor, remote_asn] + properties: + neighbor: + type: string + remote_asn: + type: integer + bgp_speaker_id: + type: ["string", "null"] + additionalProperties: true + + BgpPeerPatch: + type: object + properties: + neighbor: + type: string + remote_asn: + type: integer + bgp_speaker_id: + type: ["string", "null"] + additionalProperties: true + + BgpSpeakerCreate: + type: object + required: [endpoint] + properties: + role: + type: string + endpoint: + type: string + additionalProperties: true + + BgpSpeakerPatch: + type: object + properties: + role: + type: string + endpoint: + type: string + additionalProperties: true + + LatestRevisionPointer: + type: object + required: + - revision_id + properties: + revision_id: + $ref: "#/components/schemas/ResourceId" + published_at: + type: string + format: date-time + +paths: + /v1/health: + get: + tags: [System] + summary: Liveness + description: Проверка, что процесс API жив. Не обязана проверять БД или брокер. + operationId: getHealth + responses: + "200": + description: Процесс отвечает. + content: + application/json: + schema: + $ref: "#/components/schemas/HealthStatus" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/ready: + get: + tags: [System] + summary: Readiness + description: Готовность к трафику (БД, брокер сообщений при reference-архитектуре и т.д.). + operationId: getReady + responses: + "200": + description: Сервис готов принимать запросы. + content: + application/json: + schema: + $ref: "#/components/schemas/ReadyStatus" + "503": + description: Не готов (зависимости недоступны). + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/version: + get: + tags: [System] + summary: Версия сборки + description: Версия API и control-plane (`git_sha`, `build_time` и др.). + operationId: getVersion + responses: + "200": + description: Метаданные сборки. + content: + application/json: + schema: + $ref: "#/components/schemas/VersionInfo" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules: + get: + tags: [Modules] + summary: Список модулей + description: Модули tenant с опциональными фильтрами по типу и флагу `enabled`. + operationId: listModules + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/ModuleTypeFilter" + - $ref: "#/components/parameters/ModuleEnabledFilter" + responses: + "200": + description: Страница модулей. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/Module" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "401": + $ref: "#/components/responses/Unauthorized" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Modules] + summary: Создать модуль + description: | + Создаёт экземпляр модуля. Поле `type` задаёт вид (`AS_PREFIXES`, `CDN_CIDRS`, `DOMAINS`, `IP_RANGES`). + `module_id` в других путях — идентификатор экземпляра, не имя типа. + operationId: createModule + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ModuleCreate" + responses: + "201": + description: Модуль создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + get: + tags: [Modules] + summary: Получить модуль + operationId: getModule + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Modules] + summary: Обновить модуль + description: Частичное обновление (расписание, DoH, приоритет, `enabled` и т.д.). + operationId: patchModule + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ModulePatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Modules] + summary: Удалить модуль + description: Мягкое удаление или перевод в `enabled=false` — конкретное поведение задаётся реализацией. + operationId: deleteModule + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено или деактивировано. + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/cdn-sources: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + get: + tags: [Modules] + summary: Список CDN-источников + operationId: listCdnSources + parameters: + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/CdnSource" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Modules] + summary: Добавить CDN-источник + description: URL, `source_kind`, опционально `community_id`. + operationId: createCdnSource + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CdnSourceCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/CdnSource" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/cdn-sources/{source_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + - $ref: "#/components/parameters/SourceId" + patch: + tags: [Modules] + summary: Обновить CDN-источник + operationId: patchCdnSource + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CdnSourcePatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/CdnSource" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Modules] + summary: Удалить CDN-источник + operationId: deleteCdnSource + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/as-entries: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + get: + tags: [Modules] + summary: Список AS-записей + operationId: listAsEntries + parameters: + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/AsEntry" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Modules] + summary: Добавить AS-запись + operationId: createAsEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AsEntryCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/AsEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/as-entries/{entry_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + - $ref: "#/components/parameters/EntryId" + patch: + tags: [Modules] + summary: Обновить AS-запись + operationId: patchAsEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AsEntryCreate" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/AsEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Modules] + summary: Удалить AS-запись + operationId: deleteAsEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/domain-entries: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + get: + tags: [Modules] + summary: Список доменных записей + description: FQDN и привязка к community. + operationId: listDomainEntries + parameters: + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/DomainEntry" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Modules] + summary: Добавить доменную запись + operationId: createDomainEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DomainEntryCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/DomainEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/domain-entries/{entry_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + - $ref: "#/components/parameters/EntryId" + patch: + tags: [Modules] + summary: Обновить доменную запись + operationId: patchDomainEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DomainEntryCreate" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/DomainEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Modules] + summary: Удалить доменную запись + operationId: deleteDomainEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/ip-range-entries: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + get: + tags: [Modules] + summary: Список IP-диапазонов + description: "Только для модулей с типом IP_RANGES (статические CIDR и community_id)." + operationId: listIpRangeEntries + parameters: + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/IpRangeEntry" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Modules] + summary: Добавить IP-диапазон + operationId: createIpRangeEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/IpRangeEntryCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/IpRangeEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/ip-range-entries/{entry_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + - $ref: "#/components/parameters/EntryId" + patch: + tags: [Modules] + summary: Обновить IP-диапазон + operationId: patchIpRangeEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/IpRangeEntryCreate" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/IpRangeEntry" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Modules] + summary: Удалить IP-диапазон + operationId: deleteIpRangeEntry + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/modules/{module_id}/refresh: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/ModuleId" + post: + tags: [Modules] + summary: Запустить ingest + description: | + Запуск обновления данных модуля (CDN / DoH / AS в зависимости от типа). + Для `IP_RANGES` данные обычно только в БД: сервер может вернуть **204** (no-op) или **400**, если refresh не поддерживается — поведение фиксируется в реализации. + Рекомендуется передавать `Idempotency-Key`. + operationId: postModuleRefresh + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "202": + description: Задача поставлена в очередь. + headers: + Location: + description: URL задачи `/v1/jobs/{job_id}` + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + "204": + description: Нет операции (например модуль `IP_RANGES` и no-op). + "400": + description: Тип модуля не поддерживает refresh. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/doh-profiles: + get: + tags: [DoH profiles] + summary: Список DoH-профилей + operationId: listDohProfiles + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/DohProfile" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [DoH profiles] + summary: Создать DoH-профиль + description: URL и таймауты; секрет — через vault id или отдельный вызов установки секрета. + operationId: createDohProfile + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DohProfileCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/DohProfile" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/doh-profiles/{id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/DohProfileId" + get: + tags: [DoH profiles] + summary: Получить DoH-профиль + description: Секрет в ответе не возвращается. + operationId: getDohProfile + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/DohProfile" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [DoH profiles] + summary: Обновить DoH-профиль + operationId: patchDohProfile + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DohProfilePatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/DohProfile" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [DoH profiles] + summary: Удалить DoH-профиль + description: Допустимо только если профиль не используется модулями. + operationId: deleteDohProfile + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "409": + $ref: "#/components/responses/Conflict" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/communities: + get: + tags: [Communities] + summary: Список BGP community + operationId: listCommunities + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/BgpCommunity" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Communities] + summary: Создать community + operationId: createCommunity + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpCommunityCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpCommunity" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/communities/{id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/CommunityId" + get: + tags: [Communities] + summary: Получить community + operationId: getCommunity + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpCommunity" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Communities] + summary: Обновить community + operationId: patchCommunity + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpCommunityPatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpCommunity" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Communities] + summary: Удалить community + description: Только при отсутствии ссылок из других сущностей. + operationId: deleteCommunity + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено. + "409": + $ref: "#/components/responses/Conflict" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/peers: + get: + tags: [Peers] + summary: Список пиров + operationId: listPeers + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/SpeakerFilter" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/BgpPeer" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Peers] + summary: Создать пира + operationId: createPeer + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpPeerCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpPeer" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/peers/{id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/PeerId" + get: + tags: [Peers] + summary: Получить пира + operationId: getPeer + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpPeer" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Peers] + summary: Обновить пира + description: Политики, neighbor, ASN, привязка к `bgp_speaker_id` или `null` для всех спикеров. + operationId: patchPeer + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpPeerPatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpPeer" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Peers] + summary: Удалить или отключить пира + operationId: deletePeer + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "204": + description: Удалено / отключено. + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/speakers: + get: + tags: [Speakers] + summary: Список спикеров + description: Master / replica, endpoint и связанные поля. + operationId: listSpeakers + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/BgpSpeaker" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Speakers] + summary: Зарегистрировать спикер + description: Реплика, canary и т.д. + operationId: createSpeaker + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpSpeakerCreate" + responses: + "201": + description: Ресурс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpSpeaker" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/speakers/{id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/PeerId" + get: + tags: [Speakers] + summary: Получить спикер + description: В т.ч. `last_applied_revision_id`. + operationId: getSpeaker + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpSpeaker" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Speakers] + summary: Обновить метаданные спикера + operationId: patchSpeaker + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BgpSpeakerPatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/BgpSpeaker" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions: + get: + tags: [Revisions] + summary: История ревизий + operationId: listRevisions + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/RevisionModuleFilter" + responses: + "200": + description: Список ревизий с cursor-пагинацией; опциональный фильтр `module_id`. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/ConfigRevision" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions/{revision_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/RevisionId" + get: + tags: [Revisions] + summary: Метаданные ревизии + description: Хэш, родитель, время, артефакты. + operationId: getRevision + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigRevision" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions/{revision_id}/prefixes: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/RevisionId" + get: + tags: [Revisions] + summary: Снимок префиксов ревизии + operationId: getRevisionPrefixes + parameters: + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/PrefixSnapshotItem" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions/{revision_id}/preview: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/RevisionId" + get: + tags: [Revisions] + summary: Превью фрагментов BIRD + description: Только чтение, без apply. + operationId: getRevisionPreview + responses: + "200": + description: Текст или структурированное представление — формат задаётся реализацией. + content: + application/json: + schema: + type: object + additionalProperties: true + text/plain: + schema: + type: string + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions/{revision_id}/rollback: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/RevisionId" + post: + tags: [Revisions] + summary: Откат через новую ревизию + description: | + Создаёт **новую** ревизию с содержимым отката от указанной. Часто возвращает **202** с `Location` на задачу. + Рекомендуется `Idempotency-Key`. + operationId: postRevisionRollback + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "202": + description: Откат поставлен в очередь. + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/revisions/{revision_a}/diff/{revision_b}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/RevisionA" + - $ref: "#/components/parameters/RevisionB" + get: + tags: [Revisions] + summary: Сравнить две ревизии + description: Формат diff (JSON Patch, табличный и т.д.) определяется реализацией. + operationId: getRevisionDiff + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/RevisionDiff" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/apply: + post: + tags: [Deploy] + summary: Применить конфигурацию + description: | + Применить целевую ревизию на спикерах (политика по умолчанию или из тела). + Ответ **202** с `Location` на `/v1/jobs/{job_id}`. **Рекомендуется или обязателен** `Idempotency-Key` по политике продукта. + operationId: postApply + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/ApplyRequest" + responses: + "202": + description: Запрос принят, задача поставлена в очередь. + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/speakers/{id}/apply: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/PeerId" + post: + tags: [Deploy] + summary: Применить на одном спикере + description: Canary / точечный deploy. **202** + `Location` на job. + operationId: postSpeakerApply + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/ApplyRequest" + responses: + "202": + description: Запрос принят, задача поставлена в очередь. + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/bird/reload: + post: + tags: [Deploy] + summary: Мягкий reload BIRD + description: Опциональный явный reload политики, если отделён от apply; иначе может отсутствовать или быть частью apply. + operationId: postBirdReload + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "202": + description: Задача reload поставлена (если реализовано как async). + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + "204": + description: Reload выполнен синхронно (если так реализовано). + "501": + description: Эндпоинт не используется в данной сборке. + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/jobs: + get: + tags: [Jobs] + summary: Список задач + operationId: listJobs + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/JobStatusFilter" + - $ref: "#/components/parameters/JobKindFilter" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + required: [items, has_more] + properties: + items: + type: array + items: + $ref: "#/components/schemas/Job" + next_cursor: + type: ["string", "null"] + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/jobs/{job_id}: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/JobId" + get: + tags: [Jobs] + summary: Статус задачи + description: Прогресс, ошибка, связанные сущности в `meta`. + operationId: getJob + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/Job" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/jobs/{job_id}/cancel: + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/JobId" + post: + tags: [Jobs] + summary: Запросить отмену задачи + description: Best-effort; гарантии зависят от вида задачи. + operationId: postJobCancel + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + responses: + "202": + description: Запрос отмены принят. + content: + application/json: + schema: + $ref: "#/components/schemas/Job" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/speakers/{speaker_id}/revisions/latest: + parameters: + - $ref: "#/components/parameters/SpeakerId" + get: + tags: [Node] + summary: Последняя опубликованная ревизия для ноды + description: Вызывается **evobgp-node** с ключом роли `node`. + operationId: getSpeakerLatestRevision + security: + - bearerAuth: [] + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/LatestRevisionPointer" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/speakers/{speaker_id}/bundle/{revision_id}: + parameters: + - $ref: "#/components/parameters/SpeakerId" + - $ref: "#/components/parameters/RevisionId" + get: + tags: [Node] + summary: Скачать подписанный бандл + description: | + Архив с `manifest.json` и подписью (например Ed25519). Целостность по SHA-256 в манифесте. + `Content-Type` — `application/octet-stream` или multipart; детали — в реализации. + operationId: getSpeakerBundle + security: + - bearerAuth: [] + responses: + "200": + description: Бинарное тело бандла. + headers: + Content-Disposition: + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/nodes/enroll: + post: + tags: [Node] + summary: Регистрация ноды + description: Обмен ключами и привязка к `speaker_id`; детали протокола уточняются отдельно. + operationId: postNodeEnroll + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NodeEnrollRequest" + responses: + "200": + description: Успешная регистрация (плейсхолдер). + content: + application/json: + schema: + $ref: "#/components/schemas/NodeEnrollResponse" + "422": + $ref: "#/components/responses/UnprocessableEntity" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/settings: + get: + tags: [Settings] + summary: Получить настройки + description: KV (лимиты CDN, feature flags и т.д.). + operationId: getSettings + parameters: + - $ref: "#/components/parameters/TenantId" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/Settings" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Settings] + summary: Частично обновить настройки + description: Только роль **operator**. + operationId: patchSettings + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Settings" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/Settings" + "403": + $ref: "#/components/responses/Forbidden" + default: + $ref: "#/components/responses/DefaultProblem" diff --git a/redocly.yaml b/redocly.yaml new file mode 100644 index 0000000..2a29655 --- /dev/null +++ b/redocly.yaml @@ -0,0 +1,6 @@ +extends: + - recommended + +rules: + no-server-example.com: off + operation-4xx-response: off