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
2026-04-05 18:36:32 +07:00
parent c6687b2c91
commit acc0608432
18 changed files with 110 additions and 96 deletions
@@ -118,7 +118,7 @@ EvoBGP/
│ ├── repository/ # SQL по сущностям (tenant, module, peer, revision, jobs, …)
│ ├── domain/ # типы и правила без I/O (префиксы, community, ревизии)
│ ├── httpapi/ # роуты OpenAPI, middleware, валидация, маппинг в сервисы
│ ├── jobs/ # очередь: брокер (reference) vs PG + SKIP LOCKED (microVPS)
│ ├── jobs/ # задачи: сейчас in-memory Registry в процессе API; PG job_audit / брокер — целевое расширение (см. примечание ниже §2)
│ ├── scheduler/ # триггеры по расписанию модулей
│ ├── ingest/ # CDN, DoH, нормализация, запись в БД
│ ├── render/ # материализация префиксов, ревизия, текст артефактов BIRD
@@ -237,6 +237,8 @@ flowchart LR
> **Правило:** логика домена **одинакова**; **один движок БД — 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 — только если критичен абсолютный минимум контейнеров; иначе **не рекомендуется** как основной путь.
---
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"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() {
cfg := config.Load()
opts := httpapi.Options{
+4 -1
View File
@@ -45,7 +45,10 @@ func main() {
Store: st,
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.APIToken = apiTok
log.Printf("evobgp-scheduler: control plane HTTP mode (%s)", apiBase)
+1
View File
@@ -5,6 +5,7 @@
# docker compose --profile microvps up -d --build
#
# 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_sqlite из плана (без контейнера Postgres): требует реализации store на SQLite в приложении;
+1 -1
View File
@@ -38,7 +38,7 @@
| `birddeploy` | Логика применения конфигурации к BIRD (используется в цепочке деплоя). |
| `config` | Переменные окружения `EVOBGP_*`. |
| `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`. |
## Диаграмма: эталонный Compose (reference)
+4 -4
View File
File diff suppressed because one or more lines are too long
+10 -2
View File
@@ -699,7 +699,15 @@ components:
NodeEnrollResponse:
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
DohProfilePatch:
@@ -2259,7 +2267,7 @@ paths:
$ref: "#/components/schemas/NodeEnrollRequest"
responses:
"200":
description: Успешная регистрация (плейсхолдер).
description: Enrollment записан (node_public_key и время в meta спикера при наличии ключа).
content:
application/json:
schema:
+4 -4
View File
@@ -40,7 +40,7 @@ docker login git.shts.su
| Образ | Назначение | Страница пакета (пример) | 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-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-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` |
@@ -67,7 +67,7 @@ docker run --rm -p 8080:8080 `
## Вариант 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
cd deploy\compose
@@ -99,7 +99,7 @@ docker compose --profile microvps down -v
## Вариант 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
cd deploy\compose
@@ -139,7 +139,7 @@ cd <корень-клона-репозитория>
go run .\cmd\evobgp-api
```
Или монолит **microVPS** (тот же API плюс горутины заглушек scheduler/ingest/render/deploy):
Или монолит **microVPS** (тот же API плюс горутины тех же воркеров scheduler/ingest/render/deploy):
```powershell
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
import (
@@ -7,12 +8,12 @@ import (
"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) {
u := strings.TrimSpace(brokerURL)
if u == "" {
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
}
+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)
}
if deps == nil || deps.Store == nil {
runStub(ctx)
return
log.Fatalf("evobgp-deploy: missing store (pass deploy.Deps from BootstrapWorkers or evobgp-all)")
}
t := time.NewTicker(90 * time.Second)
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"
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"sort"
@@ -672,14 +674,72 @@ func (s *Server) handleNodeEnroll(w http.ResponseWriter, r *http.Request) {
if !s.requireNode(w, a) {
return
}
var req map[string]any
_ = json.NewDecoder(r.Body).Decode(&req)
var body struct {
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{
"status": "accepted",
"message": "enrollment stub; operator approval required in production",
"status": "enrolled",
"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 {
keys := make([]string, 0, len(m))
for k := range m {
+1 -16
View File
@@ -22,8 +22,7 @@ func Run(ctx context.Context, deps *Deps) {
cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil {
runStub(ctx)
return
log.Fatalf("evobgp-ingest: missing store (pass ingest.Deps from BootstrapWorkers or evobgp-all)")
}
hc := &http.Client{Timeout: 45 * 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"
)
// Job is the API-facing job model (maps to job_audit).
// Job is the API-facing job model (поля согласованы со схемой job_audit в миграциях; персистенция в БД пока не подключена).
type Job struct {
ID string
TenantID string
@@ -148,7 +148,9 @@ func (j *Job) Snapshot() map[string]any {
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 {
mu sync.RWMutex
byID map[string]*Job
+1 -16
View File
@@ -22,8 +22,7 @@ func Run(ctx context.Context, deps *Deps) {
cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil {
runStub(ctx)
return
log.Fatalf("evobgp-render: missing store (pass render.Deps from BootstrapWorkers or evobgp-all)")
}
t := time.NewTicker(45 * time.Second)
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.
// Migrations: see migrations/ at repository root.
// In-memory реализация для тестов и режима без БД — в internal/store.
// Миграции: каталог migrations/ в корне репозитория.
package repository
+1 -1
View File
@@ -1029,7 +1029,7 @@ func (p *Postgres) DeleteCommunity(tenantID, id string) error {
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 {
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)
+3 -19
View File
@@ -26,17 +26,15 @@ type Deps struct {
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) {
cfg := config.Load()
broker.LogConnect(ctx, cfg.BrokerURL)
if deps == nil || deps.Store == nil {
runStub(ctx)
return
log.Fatalf("evobgp-scheduler: missing store (pass scheduler.Deps with Store from BootstrapWorkers or evobgp-all)")
}
if deps.Jobs == nil && (strings.TrimSpace(deps.APIBase) == "" || strings.TrimSpace(deps.APIToken) == "") {
runStub(ctx)
return
log.Fatalf("evobgp-scheduler: need either in-process Jobs (evobgp-all) or both EVOBGP_CONTROL_PLANE_URL and EVOBGP_SCHEDULER_BEARER")
}
t := time.NewTicker(30 * time.Second)
defer t.Stop()
@@ -125,17 +123,3 @@ func postModuleRefresh(ctx context.Context, deps *Deps, moduleID, idempotencyKey
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
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:
}
}
}