docs: update architecture and API documentation to clarify job processing, in-memory registry usage, and future broker integration plans. Enhance OpenAPI specifications for enrollment responses and refine quickstart instructions for Docker setups.
CI / changes (push) Successful in 4s
CI / openapi (push) Successful in 24s
CI / go (push) Successful in 22s
CI / bird2 (push) Successful in 13s
CI / docker-images (deploy/docker/bird2/Dockerfile, evobgp-bird2) (push) Successful in 38s
CI / docker-images (deploy/docker/evobgp-agent/Dockerfile, evobgp-agent) (push) Successful in 1m8s
CI / docker-images (deploy/docker/evobgp-web/Dockerfile, evobgp-web) (push) Successful in 49s
CI / docker-images (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, evobgp-all) (push) Successful in 1m30s
CI / docker-images (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, evobgp-api) (push) Successful in 1m31s
CI / docker-images (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, evobgp-ingest) (push) Has been cancelled
CI / docker-images (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, evobgp-deploy) (push) Has been cancelled
CI / docker-images (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, evobgp-node) (push) Has been cancelled
CI / docker-images (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, evobgp-render) (push) Has been cancelled
CI / docker-images (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, evobgp-scheduler) (push) Has been cancelled

This commit is contained in:
Denozordec committed 2026-04-05 18:36:32 +07:00
1 parent c6687b2c91
commit acc0608432
18 files changed
+110 -96

No files matched your search

@@ -118,7 +118,7 @@ EvoBGP/
│ ├── repository/ # SQL по сущностям (tenant, module, peer, revision, jobs, …) │ ├── repository/ # SQL по сущностям (tenant, module, peer, revision, jobs, …)
│ ├── domain/ # типы и правила без I/O (префиксы, community, ревизии) │ ├── domain/ # типы и правила без I/O (префиксы, community, ревизии)
│ ├── httpapi/ # роуты OpenAPI, middleware, валидация, маппинг в сервисы │ ├── httpapi/ # роуты OpenAPI, middleware, валидация, маппинг в сервисы
│ ├── jobs/ # очередь: брокер (reference) vs PG + SKIP LOCKED (microVPS) │ ├── jobs/ # задачи: сейчас in-memory Registry в процессе API; PG job_audit / брокер — целевое расширение (см. примечание ниже §2)
│ ├── scheduler/ # триггеры по расписанию модулей │ ├── scheduler/ # триггеры по расписанию модулей
│ ├── ingest/ # CDN, DoH, нормализация, запись в БД │ ├── ingest/ # CDN, DoH, нормализация, запись в БД
│ ├── render/ # материализация префиксов, ревизия, текст артефактов BIRD │ ├── render/ # материализация префиксов, ревизия, текст артефактов BIRD
@@ -237,6 +237,8 @@ flowchart LR
> **Правило:** логика домена **одинакова**; **один движок БД — PostgreSQL** (одинаковые миграции и SQL). Отличаются число процессов Go, наличие брокера и **настройки** PG. > **Правило:** логика домена **одинакова**; **один движок БД — PostgreSQL** (одинаковые миграции и SQL). Отличаются число процессов Go, наличие брокера и **настройки** PG.
> **Примечание (текущий код vs целевая очередь):** исполнение async jobs идёт через **in-memory `jobs.Registry` в процессе `evobgp-api` / `evobgp-all`**; таблица `job_audit` в миграциях заложена под будущую персистенцию и идемпотентность между процессами. В **reference** Compose отдельный `evobgp-scheduler` **не** читает эту очередь, а вызывает `POST .../modules/{id}/refresh` по HTTP. NATS в compose — для будущей интеграции; см. [docs/architecture.md](../../docs/architecture.md).
> **Опция `microVPS_sqlite`:** один контейнер `evobgp-all` без PG — только если критичен абсолютный минимум контейнеров; иначе **не рекомендуется** как основной путь. > **Опция `microVPS_sqlite`:** один контейнер `evobgp-all` без PG — только если критичен абсолютный минимум контейнеров; иначе **не рекомендуется** как основной путь.
--- ---
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"evobgp/internal/scheduler" "evobgp/internal/scheduler"
) )
// microVPS entrypoint: one process — HTTP API plus in-process scheduler, ingest, render, deploy workers (shared store + job registry). // microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
func main() { func main() {
cfg := config.Load() cfg := config.Load()
opts := httpapi.Options{ opts := httpapi.Options{
+4 -1
View File
@@ -45,7 +45,10 @@ func main() {
Store: st, Store: st,
HTTP: &http.Client{Timeout: 45 * time.Second}, HTTP: &http.Client{Timeout: 45 * time.Second},
} }
if apiBase != "" && apiTok != "" { if (apiBase != "") != (apiTok != "") {
log.Fatal("evobgp-scheduler: set both EVOBGP_CONTROL_PLANE_URL and EVOBGP_SCHEDULER_BEARER for HTTP mode, or leave both empty for in-process job registry")
}
if apiBase != "" {
deps.APIBase = apiBase deps.APIBase = apiBase
deps.APIToken = apiTok deps.APIToken = apiTok
log.Printf("evobgp-scheduler: control plane HTTP mode (%s)", apiBase) log.Printf("evobgp-scheduler: control plane HTTP mode (%s)", apiBase)
+1
View File
@@ -5,6 +5,7 @@
# docker compose --profile microvps up -d --build # docker compose --profile microvps up -d --build
# #
# reference: 5× Go (api + scheduler + ingest + render + deploy) + postgres + NATS JetStream + bird2 + agent + web. # reference: 5× Go (api + scheduler + ingest + render + deploy) + postgres + NATS JetStream + bird2 + agent + web.
# Async jobs выполняются in-process в evobgp-api (jobs.Registry); NATS — задел под брокер (см. docs/architecture.md).
# microvps: evobgp-all + postgres + bird2 + agent (без брокера). # microvps: evobgp-all + postgres + bird2 + agent (без брокера).
# #
# Опция microVPS_sqlite из плана (без контейнера Postgres): требует реализации store на SQLite в приложении; # Опция microVPS_sqlite из плана (без контейнера Postgres): требует реализации store на SQLite в приложении;
+1 -1
View File
@@ -38,7 +38,7 @@
| `birddeploy` | Логика применения конфигурации к BIRD (используется в цепочке деплоя). | | `birddeploy` | Логика применения конфигурации к BIRD (используется в цепочке деплоя). |
| `config` | Переменные окружения `EVOBGP_*`. | | `config` | Переменные окружения `EVOBGP_*`. |
| `observability` | Метрики Prometheus, HTTP middleware. | | `observability` | Метрики Prometheus, HTTP middleware. |
| `broker` | Заготовка под NATS/Redis (логирование подключения в воркерах). | | `broker` | Опциональный `EVOBGP_BROKER_URL` для будущей шины; сейчас задачи только in-process (`jobs.Registry`), пакет лишь логирует факт настройки URL. |
| `pipeline` | Ingest+render в одном шаге для `module_refresh`: выборка префиксов (CDN/AS/IP/пустые DOMAINS), `CreateRenderRevision`, превью BIRD через `birdfmt`. | | `pipeline` | Ingest+render в одном шаге для `module_refresh`: выборка префиксов (CDN/AS/IP/пустые DOMAINS), `CreateRenderRevision`, превью BIRD через `birdfmt`. |
## Диаграмма: эталонный Compose (reference) ## Диаграмма: эталонный Compose (reference)
+4 -4
View File
@@ -3210,15 +3210,15 @@ data-styled.g138[id="sc-iJQrDi"]{content:"gtHWGb,"}/*!sc*/
" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"><p>Обмен ключами и привязка к <code>speaker_id</code>; детали протокола уточняются отдельно.</p> " class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"><p>Обмен ключами и привязка к <code>speaker_id</code>; детали протокола уточняются отдельно.</p>
</div></div><div class="sc-ftWlEC pzAgN"><div class="sc-cYRmzp kuPPCO"><h5 class="sc-irTswZ sc-jOQpHb fA-dGxt qtYpT">Authorizations:</h5><svg class="sc-fbJfz eiOVYa" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-jTsRVM gZngKQ"><span class="sc-dskThK ddMcul"><span class="sc-hNeXkj cLOXvd"><i>bearerAuth</i></span></span></div></div><h5 class="sc-irTswZ fA-dGxt">Request Body schema: <span class="sc-gXSCqT bzXJk">application/json</span><div class="sc-gUrTyB sc-eKYjSU sc-dVCGSo bPAaet boAHLn iuCmqI">required</div></h5><div html="" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><table class="sc-hIqOWV dybric"><tbody><tr class=""><td kind="field" title="public_key" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">public_key</span></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">string</span></div> <div><div html="" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"></div></div></div></td></tr><tr class=""><td kind="field" title="speaker_id" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">speaker_id</span></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">string</span><span class="sc-gUrTyB sc-jfSnVt bPAaet kEFcTG"> (<!-- -->ResourceId<!-- -->) </span><span class="sc-gUrTyB sc-ejdXBF bPAaet gTwxVV">^[0-9A-Za-z_-]{20,36}$</span></div> <div><div html="&lt;p&gt;UUID v7 или ULID.&lt;/p&gt; </div></div><div class="sc-ftWlEC pzAgN"><div class="sc-cYRmzp kuPPCO"><h5 class="sc-irTswZ sc-jOQpHb fA-dGxt qtYpT">Authorizations:</h5><svg class="sc-fbJfz eiOVYa" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-jTsRVM gZngKQ"><span class="sc-dskThK ddMcul"><span class="sc-hNeXkj cLOXvd"><i>bearerAuth</i></span></span></div></div><h5 class="sc-irTswZ fA-dGxt">Request Body schema: <span class="sc-gXSCqT bzXJk">application/json</span><div class="sc-gUrTyB sc-eKYjSU sc-dVCGSo bPAaet boAHLn iuCmqI">required</div></h5><div html="" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><table class="sc-hIqOWV dybric"><tbody><tr class=""><td kind="field" title="public_key" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">public_key</span></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">string</span></div> <div><div html="" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"></div></div></div></td></tr><tr class=""><td kind="field" title="speaker_id" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">speaker_id</span></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">string</span><span class="sc-gUrTyB sc-jfSnVt bPAaet kEFcTG"> (<!-- -->ResourceId<!-- -->) </span><span class="sc-gUrTyB sc-ejdXBF bPAaet gTwxVV">^[0-9A-Za-z_-]{20,36}$</span></div> <div><div html="&lt;p&gt;UUID v7 или ULID.&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>UUID v7 или ULID.</p> " class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>UUID v7 или ULID.</p>
</div></div></div></td></tr><tr class="last "><td kind="additionalProperties" title="property name*" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">property name*</span><div class="sc-gUrTyB sc-eKYjSU sc-oQLfz bPAaet boAHLn llEodL">additional property</div></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">any</span></div> <div><div html="" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"></div></div></div></td></tr></tbody></table><div><h3 class="sc-fJjTez hsJdXF">Responses</h3><div><button class="sc-caslwi brztng"><svg class="sc-fbJfz iZiZiV" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">200<!-- --> </strong><div html="&lt;p&gt;Успешная регистрация (плейсхолдер).&lt;/p&gt; </div></div></div></td></tr><tr class="last "><td kind="additionalProperties" title="property name*" class="sc-hAtEya sc-fGFwA-d kddqHa eIujjv"><span class="sc-ieZDjf bEKmDE"></span><span class="property-name">property name*</span><div class="sc-gUrTyB sc-eKYjSU sc-oQLfz bPAaet boAHLn llEodL">additional property</div></td><td class="sc-blLsxE bmauLr"><div><div><span class="sc-gUrTyB sc-kZGvTq bPAaet UMvzF"></span><span class="sc-gUrTyB sc-iMfspz bPAaet fEQrHJ">any</span></div> <div><div html="" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"></div></div></div></td></tr></tbody></table><div><h3 class="sc-fJjTez hsJdXF">Responses</h3><div><button class="sc-caslwi brztng"><svg class="sc-fbJfz iZiZiV" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">200<!-- --> </strong><div html="&lt;p&gt;Enrollment записан (node_public_key и время в meta спикера при наличии ключа).&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Успешная регистрация (плейсхолдер).</p> " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Enrollment записан (node_public_key и время в meta спикера при наличии ключа).</p>
</div></button></div><div><button class="sc-caslwi fvWYOy"><svg class="sc-fbJfz kiMFkB" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">422<!-- --> </strong><div html="&lt;p&gt;Семантическая ошибка запроса или валидации полей.&lt;/p&gt; </div></button></div><div><button class="sc-caslwi fvWYOy"><svg class="sc-fbJfz kiMFkB" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">422<!-- --> </strong><div html="&lt;p&gt;Семантическая ошибка запроса или валидации полей.&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Семантическая ошибка запроса или валидации полей.</p> " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Семантическая ошибка запроса или валидации полей.</p>
</div></button></div><div><button class="sc-caslwi fvWYOy"><svg class="sc-fbJfz kiMFkB" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">default<!-- --> </strong><div html="&lt;p&gt;Ошибка (см. тело Problem).&lt;/p&gt; </div></button></div><div><button class="sc-caslwi fvWYOy"><svg class="sc-fbJfz kiMFkB" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-fYaxgW jJkGwY">default<!-- --> </strong><div html="&lt;p&gt;Ошибка (см. тело Problem).&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Ошибка (см. тело Problem).</p> " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Ошибка (см. тело Problem).</p>
</div></button></div></div></div><div class="sc-jTrPJt sc-gLDzao dVngAA fYLqku"><div class="sc-fYzRkH dHdMVa"><button class="sc-jYvNnh iWrBta"><span type="post" class="sc-eGFuAY ejslkd http-verb post">post</span><span class="sc-GJyyy dkiPkt">/v1/nodes/enroll</span><svg class="sc-fbJfz ivEQut" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-fnxdBX dplsyJ"><div class="sc-llcuoK cNCbuV"><div html="&lt;p&gt;Базовый URL инсталляции (замените на свой)&lt;/p&gt; </div></button></div></div></div><div class="sc-jTrPJt sc-gLDzao dVngAA fYLqku"><div class="sc-fYzRkH dHdMVa"><button class="sc-jYvNnh iWrBta"><span type="post" class="sc-eGFuAY ejslkd http-verb post">post</span><span class="sc-GJyyy dkiPkt">/v1/nodes/enroll</span><svg class="sc-fbJfz ivEQut" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-fnxdBX dplsyJ"><div class="sc-llcuoK cNCbuV"><div html="&lt;p&gt;Базовый URL инсталляции (замените на свой)&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>Базовый URL инсталляции (замените на свой)</p> " class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>Базовый URL инсталляции (замените на свой)</p>
</div><div tabindex="0" role="button"><div class="sc-jnsZEx eobUac"><span>https://api.example.com</span>/v1/nodes/enroll</div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Request samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="react-tabs__tab react-tabs__tab--selected" role="tab" id="tab_R_157la_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_157la_0" tabindex="0" data-rttab="true">Payload</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_157la_0" aria-labelledby="tab_R_157la_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"public_key"</span>: <span class="token string">&quot;string&quot;</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"speaker_id"</span>: <span class="token string">&quot;01JQXYZABCDEFGHIJKLMNOPQRS&quot;</span></div></li></ul><span class="token punctuation">}</span></code></div></div></div></div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Response samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab_R_177la_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_177la_0" tabindex="0" data-rttab="true">200</li><li class="tab-error" role="tab" id="tab_R_177la_1" aria-selected="false" aria-disabled="false" aria-controls="panel_R_177la_1" data-rttab="true">422</li><li class="tab-error" role="tab" id="tab_R_177la_2" aria-selected="false" aria-disabled="false" aria-controls="panel_R_177la_2" data-rttab="true">default</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_177la_0" aria-labelledby="tab_R_177la_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><span class="token punctuation">{ }</span></code></div></div></div></div></div></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_177la_1" aria-labelledby="tab_R_177la_1"></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_177la_2" aria-labelledby="tab_R_177la_2"></div></div></div></div></div></div><div id="tag/Settings" data-section-id="tag/Settings" class="sc-eDDNvO eTiIZG"><div class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG"><h2 class="sc-qRumy bcNKdh"><a class="sc-csCMJq jSIqAu" href="#tag/Settings" aria-label="tag/Settings"></a>Settings</h2></div></div><div class="sc-hLseeT eiTXyS"><div class="sc-iKGpAq sc-cCYyou dXXcln dHaogz redoc-markdown " html="&lt;p&gt;Глобальные настройки и feature flags; изменение - только operator.&lt;/p&gt; </div><div tabindex="0" role="button"><div class="sc-jnsZEx eobUac"><span>https://api.example.com</span>/v1/nodes/enroll</div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Request samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="react-tabs__tab react-tabs__tab--selected" role="tab" id="tab_R_157la_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_157la_0" tabindex="0" data-rttab="true">Payload</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_157la_0" aria-labelledby="tab_R_157la_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"public_key"</span>: <span class="token string">&quot;string&quot;</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"speaker_id"</span>: <span class="token string">&quot;01JQXYZABCDEFGHIJKLMNOPQRS&quot;</span></div></li></ul><span class="token punctuation">}</span></code></div></div></div></div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Response samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab_R_177la_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_177la_0" tabindex="0" data-rttab="true">200</li><li class="tab-error" role="tab" id="tab_R_177la_1" aria-selected="false" aria-disabled="false" aria-controls="panel_R_177la_1" data-rttab="true">422</li><li class="tab-error" role="tab" id="tab_R_177la_2" aria-selected="false" aria-disabled="false" aria-controls="panel_R_177la_2" data-rttab="true">default</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_177la_0" aria-labelledby="tab_R_177la_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"status"</span>: <span class="token string">&quot;enrolled&quot;</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"speaker_id"</span>: <span class="token string">&quot;01JQXYZABCDEFGHIJKLMNOPQRS&quot;</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"tenant_id"</span>: <span class="token string">&quot;01JQXYZABCDEFGHIJKLMNOPQRS&quot;</span></div></li></ul><span class="token punctuation">}</span></code></div></div></div></div></div></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_177la_1" aria-labelledby="tab_R_177la_1"></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_177la_2" aria-labelledby="tab_R_177la_2"></div></div></div></div></div></div><div id="tag/Settings" data-section-id="tag/Settings" class="sc-eDDNvO eTiIZG"><div class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG"><h2 class="sc-qRumy bcNKdh"><a class="sc-csCMJq jSIqAu" href="#tag/Settings" aria-label="tag/Settings"></a>Settings</h2></div></div><div class="sc-hLseeT eiTXyS"><div class="sc-iKGpAq sc-cCYyou dXXcln dHaogz redoc-markdown " html="&lt;p&gt;Глобальные настройки и feature flags; изменение - только operator.&lt;/p&gt;
"><p>Глобальные настройки и feature flags; изменение - только operator.</p> "><p>Глобальные настройки и feature flags; изменение - только operator.</p>
</div></div></div><div id="tag/Settings/operation/getSettings" data-section-id="tag/Settings/operation/getSettings" class="sc-eDDNvO iUTsUN"><div data-section-id="operation/getSettings" id="operation/getSettings" class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG"><h2 class="sc-qRumy bcNKdh"><a class="sc-csCMJq jSIqAu" href="#tag/Settings/operation/getSettings" aria-label="tag/Settings/operation/getSettings"></a>Получить настройки<!-- --> </h2><div class="sc-iERabE dUdgLx"><div html="&lt;p&gt;KV (лимиты CDN, feature flags и т.д.).&lt;/p&gt; </div></div></div><div id="tag/Settings/operation/getSettings" data-section-id="tag/Settings/operation/getSettings" class="sc-eDDNvO iUTsUN"><div data-section-id="operation/getSettings" id="operation/getSettings" class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG"><h2 class="sc-qRumy bcNKdh"><a class="sc-csCMJq jSIqAu" href="#tag/Settings/operation/getSettings" aria-label="tag/Settings/operation/getSettings"></a>Получить настройки<!-- --> </h2><div class="sc-iERabE dUdgLx"><div html="&lt;p&gt;KV (лимиты CDN, feature flags и т.д.).&lt;/p&gt;
" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"><p>KV (лимиты CDN, feature flags и т.д.).</p> " class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"><p>KV (лимиты CDN, feature flags и т.д.).</p>
@@ -3246,7 +3246,7 @@ data-styled.g138[id="sc-iJQrDi"]{content:"gtHWGb,"}/*!sc*/
" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>Базовый URL инсталляции (замените на свой)</p> " class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"><p>Базовый URL инсталляции (замените на свой)</p>
</div><div tabindex="0" role="button"><div class="sc-jnsZEx eobUac"><span>https://api.example.com</span>/v1/settings</div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Request samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="react-tabs__tab react-tabs__tab--selected" role="tab" id="tab_R_156lq_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_156lq_0" tabindex="0" data-rttab="true">Payload</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_156lq_0" aria-labelledby="tab_R_156lq_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><span class="token punctuation">{ }</span></code></div></div></div></div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Response samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab_R_176lq_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_176lq_0" tabindex="0" data-rttab="true">200</li><li class="tab-error" role="tab" id="tab_R_176lq_1" aria-selected="false" aria-disabled="false" aria-controls="panel_R_176lq_1" data-rttab="true">403</li><li class="tab-error" role="tab" id="tab_R_176lq_2" aria-selected="false" aria-disabled="false" aria-controls="panel_R_176lq_2" data-rttab="true">default</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_176lq_0" aria-labelledby="tab_R_176lq_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><span class="token punctuation">{ }</span></code></div></div></div></div></div></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_176lq_1" aria-labelledby="tab_R_176lq_1"></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_176lq_2" aria-labelledby="tab_R_176lq_2"></div></div></div></div></div></div></div><div class="sc-emEvRt jYOHCb"></div></div></div> </div><div tabindex="0" role="button"><div class="sc-jnsZEx eobUac"><span>https://api.example.com</span>/v1/settings</div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Request samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="react-tabs__tab react-tabs__tab--selected" role="tab" id="tab_R_156lq_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_156lq_0" tabindex="0" data-rttab="true">Payload</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_156lq_0" aria-labelledby="tab_R_156lq_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><span class="token punctuation">{ }</span></code></div></div></div></div></div></div></div></div><div><h3 class="sc-kFuwaQ dEbuTz"> <!-- -->Response samples<!-- --> </h3><div class="sc-cyRfQY lbIFgo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab_R_176lq_0" aria-selected="true" aria-disabled="false" aria-controls="panel_R_176lq_0" tabindex="0" data-rttab="true">200</li><li class="tab-error" role="tab" id="tab_R_176lq_1" aria-selected="false" aria-disabled="false" aria-controls="panel_R_176lq_1" data-rttab="true">403</li><li class="tab-error" role="tab" id="tab_R_176lq_2" aria-selected="false" aria-disabled="false" aria-controls="panel_R_176lq_2" data-rttab="true">default</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel_R_176lq_0" aria-labelledby="tab_R_176lq_0"><div><div class="sc-cPlDXk gpxHhK"><span class="sc-bCDidX ccmcKc">Content type</span><div class="sc-dQelHO eMpCUl">application/json</div></div><div class="sc-hVkBjf ksuOBo"><div class="sc-cRZddz iLjyyA"><div class="sc-gjTGSz btblAa"><button><div class="sc-jegxcw fJsoyS">Copy</div></button></div><div tabindex="0" class="sc-iKGpAq dXXcln sc-jMAIzW jKIGwd"><div class="redoc-json"><code><span class="token punctuation">{ }</span></code></div></div></div></div></div></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_176lq_1" aria-labelledby="tab_R_176lq_1"></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel_R_176lq_2" aria-labelledby="tab_R_176lq_2"></div></div></div></div></div></div></div><div class="sc-emEvRt jYOHCb"></div></div></div>
<script> <script>
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.1.0","info":{"title":"EvoBGP Control Plane API","version":"0.1.0","description":"REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).\n\n**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.\nОшибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).\nПагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.\n\n**Роли** (матрица доступа): `viewer`, `editor`, `operator`, `node`. Нода использует отдельные пути и ключ с ролью `node`.\n\nЗаголовок `X-Tenant-Id` допускается только для супер-ролей (явный tenant); иначе tenant берётся из API-ключа.\n","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 <token>`. На 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"}},"CommLine truncated const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.1.0","info":{"title":"EvoBGP Control Plane API","version":"0.1.0","description":"REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).\n\n**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.\nОшибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).\nПагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.\n\n**Роли** (матрица доступа): `viewer`, `editor`, `operator`, `node`. Нода использует отдельные пути и ключ с ролью `node`.\n\nЗаголовок `X-Tenant-Id` допускается только для супер-ролей (явный tenant); иначе tenant берётся из API-ключа.\n","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 <token>`. На 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"}},"CommLine truncated
var container = document.getElementById('redoc'); var container = document.getElementById('redoc');
Redoc.hydrate(__redoc_state, container); Redoc.hydrate(__redoc_state, container);
+10 -2
View File
@@ -699,7 +699,15 @@ components:
NodeEnrollResponse: NodeEnrollResponse:
type: object type: object
description: Ответ enrollment (плейсхолдер). description: Подтверждение записи enrollment (метаданные спикера обновлены).
properties:
status:
type: string
example: enrolled
speaker_id:
$ref: "#/components/schemas/ResourceId"
tenant_id:
$ref: "#/components/schemas/ResourceId"
additionalProperties: true additionalProperties: true
DohProfilePatch: DohProfilePatch:
@@ -2259,7 +2267,7 @@ paths:
$ref: "#/components/schemas/NodeEnrollRequest" $ref: "#/components/schemas/NodeEnrollRequest"
responses: responses:
"200": "200":
description: Успешная регистрация (плейсхолдер). description: Enrollment записан (node_public_key и время в meta спикера при наличии ключа).
content: content:
application/json: application/json:
schema: schema:
+4 -4
View File
@@ -40,7 +40,7 @@ docker login git.shts.su
| Образ | Назначение | Страница пакета (пример) | Pull | | Образ | Назначение | Страница пакета (пример) | Pull |
|--------|------------|--------------------------|------| |--------|------------|--------------------------|------|
| `evobgp-api` | HTTP API (с `birdc` в образе) | [packages/…/evobgp-api](https://git.shts.su/denozord/-/packages/container/evobgp-api/latest) | `docker pull git.shts.su/denozord/evobgp-api:latest` | | `evobgp-api` | HTTP API (с `birdc` в образе) | [packages/…/evobgp-api](https://git.shts.su/denozord/-/packages/container/evobgp-api/latest) | `docker pull git.shts.su/denozord/evobgp-api:latest` |
| `evobgp-all` | Монолит microVPS: API + заглушки воркеров в одном процессе | [packages/…/evobgp-all](https://git.shts.su/denozord/-/packages/container/evobgp-all/latest) | `docker pull git.shts.su/denozord/evobgp-all:latest` | | `evobgp-all` | Монолит microVPS: API + in-process воркеры scheduler/ingest/render/deploy | [packages/…/evobgp-all](https://git.shts.su/denozord/-/packages/container/evobgp-all/latest) | `docker pull git.shts.su/denozord/evobgp-all:latest` |
| `evobgp-scheduler` | Планировщик (reference) | [packages/…/evobgp-scheduler](https://git.shts.su/denozord/-/packages/container/evobgp-scheduler/latest) | `docker pull git.shts.su/denozord/evobgp-scheduler:latest` | | `evobgp-scheduler` | Планировщик (reference) | [packages/…/evobgp-scheduler](https://git.shts.su/denozord/-/packages/container/evobgp-scheduler/latest) | `docker pull git.shts.su/denozord/evobgp-scheduler:latest` |
| `evobgp-ingest` | Ingest CDN / ETag | [packages/…/evobgp-ingest](https://git.shts.su/denozord/-/packages/container/evobgp-ingest/latest) | `docker pull git.shts.su/denozord/evobgp-ingest:latest` | | `evobgp-ingest` | Ingest CDN / ETag | [packages/…/evobgp-ingest](https://git.shts.su/denozord/-/packages/container/evobgp-ingest/latest) | `docker pull git.shts.su/denozord/evobgp-ingest:latest` |
| `evobgp-render` | Render | [packages/…/evobgp-render](https://git.shts.su/denozord/-/packages/container/evobgp-render/latest) | `docker pull git.shts.su/denozord/evobgp-render:latest` | | `evobgp-render` | Render | [packages/…/evobgp-render](https://git.shts.su/denozord/-/packages/container/evobgp-render/latest) | `docker pull git.shts.su/denozord/evobgp-render:latest` |
@@ -67,7 +67,7 @@ docker run --rm -p 8080:8080 `
## Вариант 1: Docker, профиль microvps ## Вариант 1: Docker, профиль microvps
Один процесс `evobgp-all` (HTTP API + in-process заглушки воркеров), PostgreSQL, BIRD2, `evobgp-agent`. Один процесс `evobgp-all` (HTTP API + in-process воркеры scheduler, ingest, render, deploy), PostgreSQL, BIRD2, `evobgp-agent`.
```powershell ```powershell
cd deploy\compose cd deploy\compose
@@ -99,7 +99,7 @@ docker compose --profile microvps down -v
## Вариант 2: Docker, профиль reference ## Вариант 2: Docker, профиль reference
Эталонное разбиение: отдельные контейнеры `evobgp-api`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, NATS JetStream, веб UI за nginx, опционально Prometheus. Эталонное разбиение: отдельные контейнеры `evobgp-api`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, сервис **NATS JetStream** (в compose; привязка очереди задач к брокеру — следующая итерация), веб UI за nginx, опционально Prometheus.
```powershell ```powershell
cd deploy\compose cd deploy\compose
@@ -139,7 +139,7 @@ cd <корень-клона-репозитория>
go run .\cmd\evobgp-api go run .\cmd\evobgp-api
``` ```
Или монолит **microVPS** (тот же API плюс горутины заглушек scheduler/ingest/render/deploy): Или монолит **microVPS** (тот же API плюс горутины тех же воркеров scheduler/ingest/render/deploy):
```powershell ```powershell
go run .\cmd\evobgp-all go run .\cmd\evobgp-all
+4 -3
View File
@@ -1,4 +1,5 @@
// Package broker stubs reference-profile message broker wiring (NATS JetStream / Redis; plan §2). // Package broker holds optional hooks for a future shared message bus (NATS JetStream / Redis).
// См. docs/architecture.md: в текущей сборке очередь задач in-process в API; EVOBGP_BROKER_URL зарезервирован.
package broker package broker
import ( import (
@@ -7,12 +8,12 @@ import (
"strings" "strings"
) )
// LogConnect logs a one-shot connection attempt when EVOBGP_BROKER_URL is set (full consumer/producer TBD). // LogConnect logs once when EVOBGP_BROKER_URL is set. Multi-process job delivery via a broker is not enabled in this build.
func LogConnect(ctx context.Context, brokerURL string) { func LogConnect(ctx context.Context, brokerURL string) {
u := strings.TrimSpace(brokerURL) u := strings.TrimSpace(brokerURL)
if u == "" { if u == "" {
return return
} }
log.Printf("broker: EVOBGP_BROKER_URL=%q (stub: no JetStream/Streams consumer in this binary yet)", u) log.Printf("broker: EVOBGP_BROKER_URL=%q set; control plane still uses in-process jobs.Registry (no JetStream/Redis consumer in this binary)", u)
_ = ctx _ = ctx
} }
+1 -16
View File
@@ -25,8 +25,7 @@ func Run(ctx context.Context, deps *Deps) {
log.Printf("evobgp-deploy: EVOBGP_BIRD_ACTIVE_DIR=%q (apply via API jobs when API has same env)", d) log.Printf("evobgp-deploy: EVOBGP_BIRD_ACTIVE_DIR=%q (apply via API jobs when API has same env)", d)
} }
if deps == nil || deps.Store == nil { if deps == nil || deps.Store == nil {
runStub(ctx) log.Fatalf("evobgp-deploy: missing store (pass deploy.Deps from BootstrapWorkers or evobgp-all)")
return
} }
t := time.NewTicker(90 * time.Second) t := time.NewTicker(90 * time.Second)
defer t.Stop() defer t.Stop()
@@ -65,17 +64,3 @@ func logDrift(ctx context.Context, st store.Backend) {
} }
} }
} }
func runStub(ctx context.Context) {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
log.Printf("evobgp-deploy: idle stub (no store in Deps)")
for {
select {
case <-ctx.Done():
log.Printf("evobgp-deploy: stopped")
return
case <-t.C:
}
}
}
+64 -4
View File
@@ -5,6 +5,8 @@ import (
"crypto/ed25519" "crypto/ed25519"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"io"
"net/http" "net/http"
"os" "os"
"sort" "sort"
@@ -672,14 +674,72 @@ func (s *Server) handleNodeEnroll(w http.ResponseWriter, r *http.Request) {
if !s.requireNode(w, a) { if !s.requireNode(w, a) {
return return
} }
var req map[string]any var body struct {
_ = json.NewDecoder(r.Body).Decode(&req) SpeakerID string `json:"speaker_id"`
PublicKey string `json:"public_key"`
}
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&body); err != nil && !errors.Is(err, io.EOF) {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
return
}
sid := strings.TrimSpace(body.SpeakerID)
if sid == "" {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "speaker_id is required")
return
}
sp, err := s.store.GetSpeaker(a.TenantID, sid)
if err != nil {
writeProblem(w, http.StatusNotFound, "Not Found", "speaker not found")
return
}
updates := map[string]any{
"node_enrolled_at": time.Now().UTC().Format(time.RFC3339Nano),
}
if pk := strings.TrimSpace(body.PublicKey); pk != "" {
updates["node_public_key"] = pk
}
meta, err := mergeSpeakerMetaJSON(sp.MetaJSON, updates)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "speaker meta_json must be a JSON object (or empty)")
return
}
patch := &store.SpeakerPatch{MetaJSON: &meta}
if _, err := s.store.UpdateSpeaker(a.TenantID, sid, patch); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"status": "accepted", "status": "enrolled",
"message": "enrollment stub; operator approval required in production", "speaker_id": sp.ID,
"tenant_id": a.TenantID,
}) })
} }
func mergeSpeakerMetaJSON(existing string, updates map[string]any) (string, error) {
existing = strings.TrimSpace(existing)
var m map[string]any
if existing != "" {
if err := json.Unmarshal([]byte(existing), &m); err != nil {
return "", err
}
if m == nil {
return "", errors.New("meta must be a JSON object")
}
}
if m == nil {
m = make(map[string]any)
}
for k, v := range updates {
m[k] = v
}
b, err := json.Marshal(m)
if err != nil {
return "", err
}
return string(b), nil
}
func sortedFragmentKeys(m map[string]string) []string { func sortedFragmentKeys(m map[string]string) []string {
keys := make([]string, 0, len(m)) keys := make([]string, 0, len(m))
for k := range m { for k := range m {
+1 -16
View File
@@ -22,8 +22,7 @@ func Run(ctx context.Context, deps *Deps) {
cfg := config.Load() cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL) broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil { if deps == nil || deps.Store == nil {
runStub(ctx) log.Fatalf("evobgp-ingest: missing store (pass ingest.Deps from BootstrapWorkers or evobgp-all)")
return
} }
hc := &http.Client{Timeout: 45 * time.Second} hc := &http.Client{Timeout: 45 * time.Second}
t := time.NewTicker(60 * time.Second) t := time.NewTicker(60 * time.Second)
@@ -41,17 +40,3 @@ func Run(ctx context.Context, deps *Deps) {
} }
} }
} }
func runStub(ctx context.Context) {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
log.Printf("evobgp-ingest: idle stub (no store in Deps)")
for {
select {
case <-ctx.Done():
log.Printf("evobgp-ingest: stopped")
return
case <-t.C:
}
}
}
+4 -2
View File
@@ -18,7 +18,7 @@ const (
StatusCancelled = "cancelled" StatusCancelled = "cancelled"
) )
// Job is the API-facing job model (maps to job_audit). // Job is the API-facing job model (поля согласованы со схемой job_audit в миграциях; персистенция в БД пока не подключена).
type Job struct { type Job struct {
ID string ID string
TenantID string TenantID string
@@ -148,7 +148,9 @@ func (j *Job) Snapshot() map[string]any {
return m return m
} }
// Registry tracks jobs in memory (microVPS-style single process; swap for PG + SKIP LOCKED later). // Registry — in-memory очередь и индекс по idempotency в процессе, где поднят HTTP API (evobgp-api и evobgp-all).
// Отдельные воркеры в reference-профиле не разделяют память с API: scheduler дергает refresh по HTTP; см. docs/architecture.md.
// Запись задач в PostgreSQL job_audit + SKIP LOCKED / внешний брокер — планируемое расширение (архитектурный план §2, §7.10).
type Registry struct { type Registry struct {
mu sync.RWMutex mu sync.RWMutex
byID map[string]*Job byID map[string]*Job
+1 -16
View File
@@ -22,8 +22,7 @@ func Run(ctx context.Context, deps *Deps) {
cfg := config.Load() cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL) broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil { if deps == nil || deps.Store == nil {
runStub(ctx) log.Fatalf("evobgp-render: missing store (pass render.Deps from BootstrapWorkers or evobgp-all)")
return
} }
t := time.NewTicker(45 * time.Second) t := time.NewTicker(45 * time.Second)
defer t.Stop() defer t.Stop()
@@ -45,17 +44,3 @@ func Run(ctx context.Context, deps *Deps) {
} }
} }
} }
func runStub(ctx context.Context) {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
log.Printf("evobgp-render: idle stub (no store in Deps)")
for {
select {
case <-ctx.Done():
log.Printf("evobgp-render: stopped")
return
case <-t.C:
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
// Package repository will host SQL-backed data access (plan §1: tenant, module, peer, revision, jobs). // Package repository — доступ к PostgreSQL (план §1: tenant, module, peer, revision, сущности модулей и т.д.).
// //
// The in-memory implementation used by the current API lives in internal/store. // In-memory реализация для тестов и режима без БД — в internal/store.
// Migrations: see migrations/ at repository root. // Миграции: каталог migrations/ в корне репозитория.
package repository package repository
+1 -1
View File
@@ -1029,7 +1029,7 @@ func (p *Postgres) DeleteCommunity(tenantID, id string) error {
return nil return nil
} }
// CDN / AS / domain / IP stubs: implement in postgres_sub.go to keep file size manageable. // CDN / AS / domain / IP / settings: см. postgres_entities.go.
func strPtrUUID(s *string) *string { func strPtrUUID(s *string) *string {
if s == nil || strings.TrimSpace(*s) == "" { if s == nil || strings.TrimSpace(*s) == "" {
-2
View File
@@ -556,5 +556,3 @@ func (p *Postgres) PatchGlobalSettings(tenantID string, patch map[string]any) er
} }
var _ store.Backend = (*Postgres)(nil) var _ store.Backend = (*Postgres)(nil)
var _ store.Backend = (*Postgres)(nil)
+3 -19
View File
@@ -26,17 +26,15 @@ type Deps struct {
HTTP *http.Client HTTP *http.Client
} }
// Run blocks until ctx is cancelled. When deps is nil or incomplete, falls back to connect-only stub logging. // Run blocks until ctx is cancelled. Misconfigured deps terminate the process (no idle fallback).
func Run(ctx context.Context, deps *Deps) { func Run(ctx context.Context, deps *Deps) {
cfg := config.Load() cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL) broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil { if deps == nil || deps.Store == nil {
runStub(ctx) log.Fatalf("evobgp-scheduler: missing store (pass scheduler.Deps with Store from BootstrapWorkers or evobgp-all)")
return
} }
if deps.Jobs == nil && (strings.TrimSpace(deps.APIBase) == "" || strings.TrimSpace(deps.APIToken) == "") { if deps.Jobs == nil && (strings.TrimSpace(deps.APIBase) == "" || strings.TrimSpace(deps.APIToken) == "") {
runStub(ctx) log.Fatalf("evobgp-scheduler: need either in-process Jobs (evobgp-all) or both EVOBGP_CONTROL_PLANE_URL and EVOBGP_SCHEDULER_BEARER")
return
} }
t := time.NewTicker(30 * time.Second) t := time.NewTicker(30 * time.Second)
defer t.Stop() defer t.Stop()
@@ -125,17 +123,3 @@ func postModuleRefresh(ctx context.Context, deps *Deps, moduleID, idempotencyKey
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(b))) return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(b)))
} }
func runStub(ctx context.Context) {
t := time.NewTicker(60 * time.Second)
defer t.Stop()
log.Printf("evobgp-scheduler: idle stub (no store/jobs: pass scheduler.Deps from evobgp-all or bootstrap worker)")
for {
select {
case <-ctx.Done():
log.Printf("evobgp-scheduler: stopped")
return
case <-t.C:
}
}
}