feat: auto-refresh non-IP module updates and add full project manual
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 38s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m7s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m21s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m18s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m5s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m19s
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 38s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m7s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m21s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m18s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m5s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m19s
Queue module refresh jobs after AS, domain, and CDN source mutations (including CSV imports) to keep revisions current across all source types, and introduce a comprehensive project manual covering runtime modules, flows, and API operations. Made-with: Cursor
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
| [overview.md](overview.md) | Ключевые возможности системы |
|
||||
| [quickstart.md](quickstart.md) | Быстрый запуск: готовые образы из `git.shts.su`, Docker Compose, локальный Go, веб |
|
||||
| [architecture.md](architecture.md) | Компоненты, потоки данных, пакеты |
|
||||
| [manual.md](manual.md) | Подробное руководство по модулям, процессам и API |
|
||||
| [api.md](api.md) | REST: префикс `/v1`, публичные маршруты, ссылки на OpenAPI |
|
||||
| [access.md](access.md) | Выдача доступа: API-ключи, роли, нода, CORS |
|
||||
| [openapi.yaml](openapi.yaml) | Источник правды по контракту API |
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
# Полное руководство по EvoBGP
|
||||
|
||||
Этот документ объединяет эксплуатационное и разработческое описание системы: что делает каждый процесс, как двигаются данные, какие API использовать и где искать причины инцидентов.
|
||||
|
||||
## 1. Назначение системы
|
||||
|
||||
EvoBGP управляет генерацией и применением BGP-конфигураций на основе модулей источников префиксов (`AS_PREFIXES`, `CDN_CIDRS`, `DOMAINS`, `IP_RANGES`).
|
||||
|
||||
Система разделена на:
|
||||
- **control plane**: API, БД, jobs, рендер ревизий, публикация и подписание бандлов;
|
||||
- **data plane**: BIRD и связанный агент/нода для применения ревизий.
|
||||
|
||||
## 2. Компоненты и роли бинарников (`cmd/*`)
|
||||
|
||||
### `evobgp-api`
|
||||
- Основной HTTP API.
|
||||
- Поднимает маршруты из `internal/httpapi`.
|
||||
- Работает с `store`/`repository`, jobs и аутентификацией.
|
||||
|
||||
### `evobgp-all`
|
||||
- Монолитный режим: API + scheduler + ingest + render + deploy в одном процессе.
|
||||
- Удобен для компактных окружений (`microvps`).
|
||||
|
||||
### `evobgp-scheduler`
|
||||
- Периодически запускает `module_refresh` по расписанию/интервалам.
|
||||
- В reference-профиле может стучаться в API и/или работать через store.
|
||||
|
||||
### `evobgp-ingest`
|
||||
- Периодически делает prefetch внешних CDN-источников (ETag/доступность).
|
||||
|
||||
### `evobgp-render`
|
||||
- Ведёт рендер-цикл; в режиме autopublish может назначать последнюю ревизию на спикеры.
|
||||
|
||||
### `evobgp-deploy`
|
||||
- Диагностирует drift: различия между опубликованной и применённой ревизией.
|
||||
|
||||
### `evobgp-node`
|
||||
- CLI-нода для edge: загрузка бандла, верификация подписи, применение.
|
||||
|
||||
### `evobgp-agent`
|
||||
- Локальный агент рядом с BIRD (наблюдение и служебные операции).
|
||||
|
||||
## 3. Карта внутренних модулей (`internal/*`)
|
||||
|
||||
### API и доступ
|
||||
- `internal/httpapi`: маршруты, auth, CORS, problem+json, CRUD, jobs endpoints.
|
||||
|
||||
### Данные
|
||||
- `internal/store`: бизнес-контракты бэкенда.
|
||||
- `internal/repository`: PostgreSQL-реализация.
|
||||
- `internal/db`: коннект и миграции.
|
||||
|
||||
### Jobs/pipeline
|
||||
- `internal/jobs`: очередь задач и worker.
|
||||
- `internal/pipeline`: `module_refresh`, сбор источников, материализация, рендер-превью.
|
||||
- `internal/scheduler`, `internal/ingest`, `internal/render`, `internal/deploy`: фоновые циклы.
|
||||
|
||||
### BIRD и бандлы
|
||||
- `internal/birdfmt`: генерация конфигурации BIRD.
|
||||
- `internal/birddeploy`: применение конфигурации и интеграция с `birdc`.
|
||||
- `internal/bundle`, `internal/signing`: упаковка и криптографическая проверка.
|
||||
|
||||
### Наблюдаемость и служебные
|
||||
- `internal/observability`: метрики/middleware.
|
||||
- `internal/asnresolve`: внешние резолвы ASN.
|
||||
- `internal/broker`: задел под внешний брокер.
|
||||
- `internal/config`, `internal/platform`: параметры среды и платформенные адаптеры.
|
||||
|
||||
## 4. Сквозной поток данных
|
||||
|
||||
1. Оператор меняет данные модуля (CRUD источников, peers/speakers, настройки).
|
||||
2. Включённый модуль триггерит `module_refresh`.
|
||||
3. `jobs.Worker` вызывает `pipeline.RefreshModule`.
|
||||
4. Pipeline собирает префиксы всех enabled-модулей tenant, строит materialized snapshot.
|
||||
5. Создаётся ревизия и BIRD preview.
|
||||
6. По операциям deploy/apply ревизия применяется на спикере.
|
||||
7. Нода получает бандл, проверяет подпись, применяет локально.
|
||||
|
||||
## 5. API: ключевые группы и сценарии
|
||||
|
||||
Источник истины контракта: `docs/openapi.yaml`.
|
||||
|
||||
### Основные группы endpoint-ов
|
||||
- `Modules`: модули и их общие параметры.
|
||||
- `AS Entries`, `CDN Sources`, `Domain Entries`, `IP Range Entries`: источники префиксов.
|
||||
- `Peers`, `Speakers`: сетевая топология применения.
|
||||
- `Revisions`, `Deploy`, `Jobs`: жизненный цикл ревизий и фоновых задач.
|
||||
- `Settings`: глобальные KV-настройки.
|
||||
- `Node`: edge-флоу бандлов/enrollment.
|
||||
|
||||
### Типовой сценарий оператора
|
||||
1. Создать/обновить модуль и его источники.
|
||||
2. Дождаться или инициировать refresh.
|
||||
3. Проверить ревизию и diff.
|
||||
4. Выполнить apply на целевой спикер.
|
||||
5. Проверить health/monitoring/jobs.
|
||||
|
||||
## 6. Настройки и доступ
|
||||
|
||||
### Аутентификация/роли
|
||||
- Роли и правила доступа: `docs/access.md`.
|
||||
- Для мутаций критичных сущностей требуется `editor`/`operator`.
|
||||
|
||||
### Настройки (`/v1/settings`)
|
||||
- KV c ключами BIRD и дополнительными feature flags.
|
||||
- Ключевые параметры BIRD: `bird_router_id`, `bird_local_ipv4`, `bird_local_ipv6`, `bird_local_asn`, `bird_bgp_source_ipv4`, `bird_bgp_source_ipv6`.
|
||||
|
||||
## 7. Эксплуатация и runbook
|
||||
|
||||
### Что проверять при инцидентах
|
||||
1. Статус API и БД.
|
||||
2. Состояние jobs (`module_refresh`, `deploy_apply`), ошибки в job meta.
|
||||
3. Состояние внешних источников (CDN/DoH/ASN).
|
||||
4. Состояние BIRD и применённой ревизии на спикере.
|
||||
|
||||
### Частые причины проблем
|
||||
- Невалидные данные источников (непарсящийся JSON/plaintext для CDN).
|
||||
- Неполные настройки BIRD.
|
||||
- Ошибки внешних upstream (RIPEstat/DoH/CDN).
|
||||
- Расхождение published/applied ревизии.
|
||||
|
||||
## 8. Разработка и расширение
|
||||
|
||||
### Где вносить изменения
|
||||
- Новый endpoint: `internal/httpapi` + обновление `docs/openapi.yaml`.
|
||||
- Новая логика источника: `internal/pipeline` + соответствующие CRUD/store/repository.
|
||||
- Новая операция UI: `web/src/routes/*` и `web/src/lib/components/*`.
|
||||
|
||||
### Рекомендации по качеству
|
||||
- Для изменений API всегда обновлять `docs/openapi.yaml`.
|
||||
- Для изменений UI держаться единого набора компонентов shadcn-svelte и Lucide.
|
||||
- Для pipeline-изменений добавлять метрики стадии и явные trigger-метки job.
|
||||
|
||||
## 9. Индекс исходников (быстрый вход)
|
||||
|
||||
- Архитектура: `docs/architecture.md`
|
||||
- API обзор: `docs/api.md`
|
||||
- Контракт API: `docs/openapi.yaml`
|
||||
- Доступ/роли: `docs/access.md`
|
||||
- Web запуск: `web/README.md`
|
||||
- Compose: `deploy/compose/docker-compose.yaml`
|
||||
|
||||
@@ -254,11 +254,13 @@ func (s *Server) handlePostCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.CreateCDNSource(a.TenantID, r.PathValue("module_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.CreateCDNSource(a.TenantID, mid, &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_create")
|
||||
writeJSON(w, http.StatusCreated, cdnSourceJSON(x))
|
||||
}
|
||||
|
||||
@@ -272,11 +274,13 @@ func (s *Server) handlePatchCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.UpdateCDNSource(a.TenantID, r.PathValue("module_id"), r.PathValue("source_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.UpdateCDNSource(a.TenantID, mid, r.PathValue("source_id"), &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_patch")
|
||||
writeJSON(w, http.StatusOK, cdnSourceJSON(x))
|
||||
}
|
||||
|
||||
@@ -285,10 +289,12 @@ func (s *Server) handleDeleteCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteCDNSource(a.TenantID, r.PathValue("module_id"), r.PathValue("source_id")); err != nil {
|
||||
mid := r.PathValue("module_id")
|
||||
if err := s.store.DeleteCDNSource(a.TenantID, mid, r.PathValue("source_id")); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_delete")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -344,11 +350,13 @@ func (s *Server) handlePostAS(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.CreateASEntry(a.TenantID, r.PathValue("module_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.CreateASEntry(a.TenantID, mid, &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_create")
|
||||
writeJSON(w, http.StatusCreated, asEntryJSON(x))
|
||||
}
|
||||
|
||||
@@ -362,11 +370,13 @@ func (s *Server) handlePatchAS(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.UpdateASEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.UpdateASEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_patch")
|
||||
writeJSON(w, http.StatusOK, asEntryJSON(x))
|
||||
}
|
||||
|
||||
@@ -375,10 +385,12 @@ func (s *Server) handleDeleteAS(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteASEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
||||
mid := r.PathValue("module_id")
|
||||
if err := s.store.DeleteASEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_delete")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -419,11 +431,13 @@ func (s *Server) handlePostDomain(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.CreateDomainEntry(a.TenantID, r.PathValue("module_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.CreateDomainEntry(a.TenantID, mid, &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_create")
|
||||
writeJSON(w, http.StatusCreated, domainEntryJSON(x))
|
||||
}
|
||||
|
||||
@@ -437,11 +451,13 @@ func (s *Server) handlePatchDomain(w http.ResponseWriter, r *http.Request) {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
x, err := s.store.UpdateDomainEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
||||
mid := r.PathValue("module_id")
|
||||
x, err := s.store.UpdateDomainEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_patch")
|
||||
writeJSON(w, http.StatusOK, domainEntryJSON(x))
|
||||
}
|
||||
|
||||
@@ -450,10 +466,12 @@ func (s *Server) handleDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteDomainEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
||||
mid := r.PathValue("module_id")
|
||||
if err := s.store.DeleteDomainEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_delete")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -713,6 +731,9 @@ func (s *Server) handleImportModuleEntriesCSV(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
imported++
|
||||
}
|
||||
if imported > 0 {
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "as_entry_import_csv")
|
||||
}
|
||||
case "DOMAINS":
|
||||
for i := start; i < len(rows); i++ {
|
||||
rec := rows[i]
|
||||
@@ -740,6 +761,9 @@ func (s *Server) handleImportModuleEntriesCSV(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
imported++
|
||||
}
|
||||
if imported > 0 {
|
||||
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "domain_entry_import_csv")
|
||||
}
|
||||
case "IP_RANGES":
|
||||
for i := start; i < len(rows); i++ {
|
||||
rec := rows[i]
|
||||
|
||||
Reference in New Issue
Block a user