From 1e77e1a2b8ffe79a53fd871c6d9a410ea16e1c22 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 7 Apr 2026 21:09:38 +0700 Subject: [PATCH] feat: auto-refresh non-IP module updates and add full project manual 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 --- docs/README.md | 1 + docs/manual.md | 142 ++++++++++++++++++++++++++++++++ internal/httpapi/routes_crud.go | 42 ++++++++-- 3 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 docs/manual.md diff --git a/docs/README.md b/docs/README.md index 68912ea..233e4d8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/docs/manual.md b/docs/manual.md new file mode 100644 index 0000000..94934fe --- /dev/null +++ b/docs/manual.md @@ -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` + diff --git a/internal/httpapi/routes_crud.go b/internal/httpapi/routes_crud.go index d852540..5abe5f9 100644 --- a/internal/httpapi/routes_crud.go +++ b/internal/httpapi/routes_crud.go @@ -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]