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]