Compare commits

..
130 Commits
Author SHA1 Message Date
Denozordec 0c11ecfa48 feat(web): enhance module management UI with new localization and state handling
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 31s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / changes (push) Successful in 7s
CI / release (push) Successful in 3m30s
- Added new utility functions for module state representation in Russian, improving localization.
- Introduced derived states for counting enabled and disabled modules, enhancing user insights.
- Updated module management components to display last updated timestamps and improved descriptions.
- Refactored imports to utilize core UI components for better maintainability and consistency.
2026-05-20 12:23:16 +07:00
Denozordec ab8660ac42 feat(web): enhance operations UI with new job status handling and component updates
- Added a new function for determining job status badge variants to improve UI consistency.
- Refactored imports to utilize the core UI library for better maintainability.
- Updated job report and filters components to enhance user experience and streamline functionality.
- Improved layout and responsiveness across operations-related components.
2026-05-20 12:14:33 +07:00
Denozordec ce747673fd feat(web): refactor module display logic and enhance UI components
- Introduced new utility functions for date formatting and module interval labeling.
- Replaced inline badge variant logic with a dedicated function for improved readability.
- Updated imports to streamline component usage and enhance maintainability.
- Enhanced the overall structure of the module display for better clarity and user experience.
2026-05-20 12:10:11 +07:00
Denozordec e558570967 feat(web): enhance monitoring page with new status tracking and UI components
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 28s
CI / go (push) Has been skipped
CI / changes (push) Successful in 8s
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m36s
- Refactored monitoring page to utilize new UI components from the core library.
- Added detailed job status tracking and error handling features.
- Improved overall layout and responsiveness of the monitoring interface.
- Introduced new derived states for better management of health and job statuses.
- Updated imports to streamline component usage and enhance maintainability.
2026-05-20 11:41:58 +07:00
DenozordecandCursor b6b081b04f style(web): format app-version.svelte for prettier
CI / openapi (push) Has been skipped
CI / commitlint (push) Has been skipped
CI / web (push) Successful in 28s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / changes (push) Successful in 7s
CI / release (push) Successful in 3m45s
Импорт в одну строку — иначе падает npm run lint в CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:21:13 +07:00
DenozordecandCursor 13b4c20148 fix(web): show semver on monitoring from legacy /v1/version
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Failing after 35s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / changes (push) Successful in 7s
CI / release (push) Has been skipped
На проде API отдаёт только api_version и git_sha=unknown без поля version;
карточка показывала SHA вместо semver. Общий хелпер version-info.ts:
version → api_version, placeholder SHA не в заголовке.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:13:15 +07:00
Denozordec 700d415422 chore: update CI workflow and release process
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 30s
CI / go (push) Successful in 25s
CI / bird2 (push) Successful in 15s
CI / changes (push) Successful in 7s
CI / release (push) Successful in 19s
- Replaced `@semantic-release/git` with `@semantic-release/exec` for handling release commands.
- Updated `.releaserc.json` to reflect changes in release command execution.
- Modified CI workflow to streamline the release process, including tagging and Docker image publishing.
- Removed obsolete publish workflow and integrated its functionality into the main CI workflow.
- Enhanced documentation to clarify the new release process and CI pipeline structure.
2026-05-20 01:05:22 +07:00
semantic-release-bot 6ca34b9008 chore(release): 1.0.0 [skip ci]
## 1.0.0 (2026-05-19)

* feat: add @tanstack/table-core dependency and integrate module name mapping in operations page. Upda ([b140dcb](http://192.168.100.56:3000/denozord/EvoBGP/commit/b140dcb))
* feat: add @types/node and undici-types to devDependencies in package.json and package-lock.json for  ([d92a06c](http://192.168.100.56:3000/denozord/EvoBGP/commit/d92a06c))
* feat: add aggregated router-lists catalog endpoint and update module listing filters ([c811c43](http://192.168.100.56:3000/denozord/EvoBGP/commit/c811c43))
* feat: add auto-updater service for Docker stack with configurable settings ([ae9fbe3](http://192.168.100.56:3000/denozord/EvoBGP/commit/ae9fbe3))
* feat: add bird configuration preview functionality with tabbed interface for fragments and prefixes. ([1969a00](http://192.168.100.56:3000/denozord/EvoBGP/commit/1969a00))
* feat: add BIRD status endpoint and integrate birdc checks into job processing. Enhance UI to display ([94a4c6a](http://192.168.100.56:3000/denozord/EvoBGP/commit/94a4c6a))
* feat: add CSV import and export functionality for module entries. Implement endpoints for exporting  ([bae2d68](http://192.168.100.56:3000/denozord/EvoBGP/commit/bae2d68))
* feat: add debug logging and optimize database queries in Postgres repository ([6d0e214](http://192.168.100.56:3000/denozord/EvoBGP/commit/6d0e214))
* feat: add diagnostic log endpoint for revisions and implement download functionality ([2260ecf](http://192.168.100.56:3000/denozord/EvoBGP/commit/2260ecf))
* feat: add enabled state to BGP peer types and UI. Implement peer enabling toggle in the network page ([80965ad](http://192.168.100.56:3000/denozord/EvoBGP/commit/80965ad))
* feat: add last refreshed timestamp to modules and CDN sources ([1e13ef9](http://192.168.100.56:3000/denozord/EvoBGP/commit/1e13ef9))
* feat: add live BGP protocol state tracking to peer listing ([8a50ba4](http://192.168.100.56:3000/denozord/EvoBGP/commit/8a50ba4))
* feat: add optional name field to BGP peer creation and update UI ([1a61ae2](http://192.168.100.56:3000/denozord/EvoBGP/commit/1a61ae2))
* feat: add optional name field to DoH profile schema and types ([6bee5ad](http://192.168.100.56:3000/denozord/EvoBGP/commit/6bee5ad))
* feat: add smart aggregation for IPv4 CIDR prefixes in refresh pipeline. Implement functionality to a ([0428b83](http://192.168.100.56:3000/denozord/EvoBGP/commit/0428b83))
* feat: add strict bind configuration to BGP templates. Enhance BGP template rendering by including 's ([07f2ce5](http://192.168.100.56:3000/denozord/EvoBGP/commit/07f2ce5))
* feat: add tenant refresh functionality to HTTP API and job processing ([7a2b015](http://192.168.100.56:3000/denozord/EvoBGP/commit/7a2b015))
* feat: auto-refresh non-IP module updates and add full project manual ([1e77e1a](http://192.168.100.56:3000/denozord/EvoBGP/commit/1e77e1a))
* feat: enhance API client with pagination and new data fetching functions. Implement `apiPageAll` for ([9f0b128](http://192.168.100.56:3000/denozord/EvoBGP/commit/9f0b128))
* feat: enhance BGP template and peer configuration handling. Introduce policies_json parameter for lo ([be8e7b5](http://192.168.100.56:3000/denozord/EvoBGP/commit/be8e7b5))
* feat: enhance CDN source management with last refreshed timestamp ([4776434](http://192.168.100.56:3000/denozord/EvoBGP/commit/4776434))
* feat: enhance community attribute handling in BIRD format. Add validation for community route bodies ([b541794](http://192.168.100.56:3000/denozord/EvoBGP/commit/b541794))
* feat: enhance community label handling and improve UI localization across operations pages. Introduc ([53fd527](http://192.168.100.56:3000/denozord/EvoBGP/commit/53fd527))
* feat: enhance deployment configuration for secure Web UI with Traefik integration ([4acd685](http://192.168.100.56:3000/denozord/EvoBGP/commit/4acd685))
* feat: enhance DoH profile management and resolver policy in modules ([34ecc5c](http://192.168.100.56:3000/denozord/EvoBGP/commit/34ecc5c))
* feat: enhance EvoBGP with new command-line options for the evobgp-agent, including a watch command f ([6a55f72](http://192.168.100.56:3000/denozord/EvoBGP/commit/6a55f72))
* feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and  ([bf52b21](http://192.168.100.56:3000/denozord/EvoBGP/commit/bf52b21))
* feat: enhance job detail handling in UI with caching and loading states. Introduce SvelteMap for job ([05bc5c4](http://192.168.100.56:3000/denozord/EvoBGP/commit/05bc5c4))
* feat: enhance job detail UI with improved overflow handling and layout adjustments. Update card comp ([67ce135](http://192.168.100.56:3000/denozord/EvoBGP/commit/67ce135))
* feat: enhance job filtering and display functionality in operations page. Introduce job search and f ([e1c4cbc](http://192.168.100.56:3000/denozord/EvoBGP/commit/e1c4cbc))
* feat: enhance job processing with detailed logging and UI improvements. Introduce revision log entri ([c48cc4e](http://192.168.100.56:3000/denozord/EvoBGP/commit/c48cc4e))
* feat: enhance monitoring page with detailed status tracking and error handling ([458a885](http://192.168.100.56:3000/denozord/EvoBGP/commit/458a885))
* feat: enhance observability with prefix aggregation metrics ([07b97bd](http://192.168.100.56:3000/denozord/EvoBGP/commit/07b97bd))
* feat: enhance peer neighbor handling by introducing NormalizePeerNeighborString function. Update Cre ([80f3a8d](http://192.168.100.56:3000/denozord/EvoBGP/commit/80f3a8d))
* feat: enhance RefreshModule to aggregate prefixes from all enabled tenant modules. Update documentat ([431c6a9](http://192.168.100.56:3000/denozord/EvoBGP/commit/431c6a9))
* feat: enhance RefreshModule to support legacy ASN resolution and improve prefix handling. Introduce  ([e1aaa18](http://192.168.100.56:3000/denozord/EvoBGP/commit/e1aaa18))
* feat: enhance settings page layout and section management ([d91667f](http://192.168.100.56:3000/denozord/EvoBGP/commit/d91667f))
* feat: enhance UI components with improved styling and layout adjustments. Update button sizes, text  ([6a72842](http://192.168.100.56:3000/denozord/EvoBGP/commit/6a72842))
* feat: enhance worker functionality by adding job queuing for deploy_apply. Introduce a Registry fiel ([42d956d](http://192.168.100.56:3000/denozord/EvoBGP/commit/42d956d))
* feat: implement AS holder name resolution and enhance ASEntry structure. Add ASHolderName function t ([c958d5a](http://192.168.100.56:3000/denozord/EvoBGP/commit/c958d5a))
* feat: implement bulk selection and deletion for modules and entries. Add functionality for selecting ([e816d17](http://192.168.100.56:3000/denozord/EvoBGP/commit/e816d17))
* feat: implement CDN source preview functionality and enhance data handling. Add a new endpoint for p ([e20c9f3](http://192.168.100.56:3000/denozord/EvoBGP/commit/e20c9f3))
* feat: implement countAll function for API data retrieval ([40c8a8d](http://192.168.100.56:3000/denozord/EvoBGP/commit/40c8a8d))
* feat: implement domain resolution via DoH in refresh pipeline. Enhance the collectModulePrefixRows f ([678bd27](http://192.168.100.56:3000/denozord/EvoBGP/commit/678bd27))
* feat: implement effective peer enabling logic during peer creation. Update CreatePeer methods in Pos ([3e85431](http://192.168.100.56:3000/denozord/EvoBGP/commit/3e85431))
* feat: implement module refresh job management with concurrency control. Add CountOtherActiveModuleRe ([149fb0a](http://192.168.100.56:3000/denozord/EvoBGP/commit/149fb0a))
* feat: implement peer reconciliation job for BGP peers ([3b17228](http://192.168.100.56:3000/denozord/EvoBGP/commit/3b17228))
* feat: implement revision retention settings and pruning logic ([4f50350](http://192.168.100.56:3000/denozord/EvoBGP/commit/4f50350))
* feat: implement theme selection and automatic theme application based on user preference. Update App ([b70a643](http://192.168.100.56:3000/denozord/EvoBGP/commit/b70a643))
* feat: implement versioning and release management in the project ([bb334b1](http://192.168.100.56:3000/denozord/EvoBGP/commit/bb334b1))
* feat: improve BGP protocol summary handling by introducing isBGPProtocolSummaryRow function. Update  ([399622c](http://192.168.100.56:3000/denozord/EvoBGP/commit/399622c))
* feat: improve job detail UI with enhanced spacing and layout adjustments. Update table cell classes  ([97908b5](http://192.168.100.56:3000/denozord/EvoBGP/commit/97908b5))
* feat: improve peer speaker assignment UX in network settings ([f877ec8](http://192.168.100.56:3000/denozord/EvoBGP/commit/f877ec8))
* feat: improve UI components with enhanced overflow handling and layout adjustments. Update card and  ([0ca5542](http://192.168.100.56:3000/denozord/EvoBGP/commit/0ca5542))
* feat: improve UI layout and responsiveness in module and operations pages. Refactor component struct ([233f29b](http://192.168.100.56:3000/denozord/EvoBGP/commit/233f29b))
* feat: redesign settings page with structured controls ([2da6d49](http://192.168.100.56:3000/denozord/EvoBGP/commit/2da6d49))
* feat: refactor materialization logic to group static routes by community in the refresh pipeline. In ([1c5a9c9](http://192.168.100.56:3000/denozord/EvoBGP/commit/1c5a9c9))
* feat: refactor operations page to enhance UI and component structure. Remove unused card components  ([88226f6](http://192.168.100.56:3000/denozord/EvoBGP/commit/88226f6))
* feat: refine BGP template handling and documentation updates. Remove local IP requirements in BGP te ([6266a39](http://192.168.100.56:3000/denozord/EvoBGP/commit/6266a39))
* feat: refine job detail UI with improved layout and scrolling capabilities. Enhance spacing and over ([57c09ac](http://192.168.100.56:3000/denozord/EvoBGP/commit/57c09ac))
* feat: update BGP community structure and related documentation. Refactor BGP community handling by r ([a2cbfac](http://192.168.100.56:3000/denozord/EvoBGP/commit/a2cbfac))
* feat: update BGP template configuration and OpenAPI documentation. Change import settings from 'all' ([133ea93](http://192.168.100.56:3000/denozord/EvoBGP/commit/133ea93))
* feat: update BIRD installation process to build from source. Modify CI workflow to ensure BIRD 2.14  ([e2002ba](http://192.168.100.56:3000/denozord/EvoBGP/commit/e2002ba))
* feat: update CI configuration and Dockerfiles for improved build efficiency ([80229b3](http://192.168.100.56:3000/denozord/EvoBGP/commit/80229b3))
* feat: update collectModulePrefixRows to support prior snapshots and enhance CDN/domain prefix collec ([ee364c8](http://192.168.100.56:3000/denozord/EvoBGP/commit/ee364c8))
* feat: update Go module dependencies and enhance DNS resolution in refresh pipeline. Upgrade Go versi ([655d750](http://192.168.100.56:3000/denozord/EvoBGP/commit/655d750))
* feat: update OpenAPI documentation and internal logic for BIRD settings. Enhance the description of  ([62bf987](http://192.168.100.56:3000/denozord/EvoBGP/commit/62bf987))
* feat: update RevisionDiff type and enhance OperationsDiffTab component for better diff handling. Int ([4aaaa30](http://192.168.100.56:3000/denozord/EvoBGP/commit/4aaaa30))
* feat: update Traefik deployment configuration for Let's Encrypt and logging ([39ba976](http://192.168.100.56:3000/denozord/EvoBGP/commit/39ba976))
* docs: add router-lists-ui integration documentation to README ([b7a8aab](http://192.168.100.56:3000/denozord/EvoBGP/commit/b7a8aab))
* docs: add Web UI section to EvoBGP architecture plan, detailing Svelte and shadcn-svelte integration ([272542b](http://192.168.100.56:3000/denozord/EvoBGP/commit/272542b))
* docs: enhance EvoBGP architecture plan with new risk mitigation strategies, testing matrix for BIRD2 ([3992d01](http://192.168.100.56:3000/denozord/EvoBGP/commit/3992d01))
* docs: enhance README and CI workflow documentation for Docker image building and publishing process, ([9eb3e33](http://192.168.100.56:3000/denozord/EvoBGP/commit/9eb3e33))
* docs: enhance README and CI workflow to clarify Docker image build conditions and Gitea Container Re ([c6687b2](http://192.168.100.56:3000/denozord/EvoBGP/commit/c6687b2))
* docs: openapi.html со встроенным Redoc; инструкция для Gitea и скрипт сборки ([a754e1d](http://192.168.100.56:3000/denozord/EvoBGP/commit/a754e1d))
* docs: update AGENTS and README to include guidelines for Conventional Commits ([4c23232](http://192.168.100.56:3000/denozord/EvoBGP/commit/4c23232))
* docs: update AGENTS and README with engineering rules and guidelines ([aff27e8](http://192.168.100.56:3000/denozord/EvoBGP/commit/aff27e8))
* docs: update architecture and API documentation to clarify job processing, in-memory registry usage, ([acc0608](http://192.168.100.56:3000/denozord/EvoBGP/commit/acc0608))
* docs: update EvoBGP architecture plan with detailed REST API section, including general agreements,  ([1a86d6c](http://192.168.100.56:3000/denozord/EvoBGP/commit/1a86d6c))
* docs: update EvoBGP architecture plan with new repository structure, refined sections, and detailed  ([4480d64](http://192.168.100.56:3000/denozord/EvoBGP/commit/4480d64))
* docs: update README and CI workflow to clarify Docker workspace mounting for bird2 job ([6be46db](http://192.168.100.56:3000/denozord/EvoBGP/commit/6be46db))
* docs: update README and CI workflow to clarify Gitea Registry login process and improve Docker image ([f1b33b9](http://192.168.100.56:3000/denozord/EvoBGP/commit/f1b33b9))
* docs: update README and Docker Compose configurations to clarify usage of pre-built images from the  ([7fac79c](http://192.168.100.56:3000/denozord/EvoBGP/commit/7fac79c))
* docs: update README to include information about the EvoBGP web interface, linking to quickstart and ([5d21f01](http://192.168.100.56:3000/denozord/EvoBGP/commit/5d21f01))
* docs: статическая Redoc-страница OpenAPI (для просмотра в Gitea без встроенного рендера) ([5c9d2e2](http://192.168.100.56:3000/denozord/EvoBGP/commit/5c9d2e2))
* docs: убрать типографское тире и невидимый Unicode из текстов API ([fbe9cd3](http://192.168.100.56:3000/denozord/EvoBGP/commit/fbe9cd3))
* refactor: adjust BootstrapWorkers function call in server.go to change the order of returned values, ([032fa99](http://192.168.100.56:3000/denozord/EvoBGP/commit/032fa99))
* refactor: consolidate module interval display logic and improve edit form handling ([1f85654](http://192.168.100.56:3000/denozord/EvoBGP/commit/1f85654))
* refactor: enhance bird metrics polling with context support ([f6b94a4](http://192.168.100.56:3000/denozord/EvoBGP/commit/f6b94a4))
* refactor: enhance CI workflow for granular change detection by module and improve idempotency key ge ([54eaad3](http://192.168.100.56:3000/denozord/EvoBGP/commit/54eaad3))
* refactor: enhance job status checks in worker tests ([6107c3e](http://192.168.100.56:3000/denozord/EvoBGP/commit/6107c3e))
* refactor: enhance JSON struct tags for various data models in backend and memory store. Update field ([4a860a0](http://192.168.100.56:3000/denozord/EvoBGP/commit/4a860a0))
* refactor: enhance module display and input handling in module pages ([25374a6](http://192.168.100.56:3000/denozord/EvoBGP/commit/25374a6))
* refactor: enhance module interval label logic and add reset buttons in edit forms ([b99fc17](http://192.168.100.56:3000/denozord/EvoBGP/commit/b99fc17))
* refactor: improve code consistency and formatting across components ([c263fd5](http://192.168.100.56:3000/denozord/EvoBGP/commit/c263fd5))
* refactor: improve handling of deployable fragments in ApplyRevision and buildPreviewFragments functi ([6932271](http://192.168.100.56:3000/denozord/EvoBGP/commit/6932271))
* refactor: improve module save functionality and nullable value handling ([05ec176](http://192.168.100.56:3000/denozord/EvoBGP/commit/05ec176))
* refactor: improve PATCH handling for module updates with nullable field support ([e700f90](http://192.168.100.56:3000/denozord/EvoBGP/commit/e700f90))
* refactor: optimize job listing and counting logic for concurrency safety. Separate tenant filtering  ([d83efe2](http://192.168.100.56:3000/denozord/EvoBGP/commit/d83efe2))
* refactor: phase 2 structural alignment — reports, importer, nodecli, pagination ([250cdea](http://192.168.100.56:3000/denozord/EvoBGP/commit/250cdea))
* refactor: remove strict bind configuration from BGP templates and update related logic. Eliminate 's ([482eeb1](http://192.168.100.56:3000/denozord/EvoBGP/commit/482eeb1))
* refactor: streamline Docker build process in CI configuration and documentation ([6025f38](http://192.168.100.56:3000/denozord/EvoBGP/commit/6025f38))
* refactor: streamline edit form handling for community and DoH profile selections ([1765ab8](http://192.168.100.56:3000/denozord/EvoBGP/commit/1765ab8))
* refactor: streamline module refresh process and enhance revision handling ([10e2415](http://192.168.100.56:3000/denozord/EvoBGP/commit/10e2415))
* refactor: switch BIRD installation from source to Ubuntu package. Update CI workflow and Dockerfiles ([0b3ceaa](http://192.168.100.56:3000/denozord/EvoBGP/commit/0b3ceaa))
* refactor: update AS entry handling in API and database to support ASN-only entries. Remove prefix fi ([1db6b5b](http://192.168.100.56:3000/denozord/EvoBGP/commit/1db6b5b))
* refactor: update Docker build context and paths in CI configuration and documentation ([7d52a4a](http://192.168.100.56:3000/denozord/EvoBGP/commit/7d52a4a))
* refactor: update Docker build variable syntax in CI configuration and documentation ([27e01d0](http://192.168.100.56:3000/denozord/EvoBGP/commit/27e01d0))
* refactor: update job kind display logic in schedule page ([dc1f4a5](http://192.168.100.56:3000/denozord/EvoBGP/commit/dc1f4a5))
* refactor: update ListModules and GetModule methods in Postgres repository to include cron expression ([73d0003](http://192.168.100.56:3000/denozord/EvoBGP/commit/73d0003))
* refactor: update Switch component styles and structure for improved accessibility and responsiveness ([d4282c2](http://192.168.100.56:3000/denozord/EvoBGP/commit/d4282c2))
* refactor: update worker processes in EvoBGP to accept dependencies for shared store and job registry ([b7968db](http://192.168.100.56:3000/denozord/EvoBGP/commit/b7968db))
* ci: add conditional job for OpenAPI linting based on file changes; update README with workflow detai ([3adfc49](http://192.168.100.56:3000/denozord/EvoBGP/commit/3adfc49))
* ci: phase 4 automation — web job, lint-httpapi, golangci ([64516c3](http://192.168.100.56:3000/denozord/EvoBGP/commit/64516c3))
* ci: update README and CI workflow to improve bird2 job installation process and clarify Docker usage ([eddc6b8](http://192.168.100.56:3000/denozord/EvoBGP/commit/eddc6b8))
* chore: phase 3 code quality — pipeline prefixes, godoc baseline ([ea65451](http://192.168.100.56:3000/denozord/EvoBGP/commit/ea65451))
* chore: update CI configuration to disable filesystem entitlement for Docker builds ([9ab7de6](http://192.168.100.56:3000/denozord/EvoBGP/commit/9ab7de6))
* chore: update package dependencies and enhance AppShell component with new navigation items and tool ([7d85fef](http://192.168.100.56:3000/denozord/EvoBGP/commit/7d85fef))
* chore: upgrade Go version to 1.24 in Dockerfiles for evobgp-agent and gobinary. This change ensures  ([4ba5b03](http://192.168.100.56:3000/denozord/EvoBGP/commit/4ba5b03))
* fix: adjust Postgres resource limits and healthcheck parameters. Increase memory limit to 256M and m ([20e674c](http://192.168.100.56:3000/denozord/EvoBGP/commit/20e674c))
* fix: improve input handling and validation for settings fields ([1386b63](http://192.168.100.56:3000/denozord/EvoBGP/commit/1386b63))
* fix: improve sidebar and navigation layout in AppShell component ([9f02128](http://192.168.100.56:3000/denozord/EvoBGP/commit/9f02128))
* fix: update BGP configuration in test scenarios to use new IPv6 addresses ([9fd9745](http://192.168.100.56:3000/denozord/EvoBGP/commit/9fd9745))
* fix: update bird build script and Dockerfile to include libncurses dependency ([0cb5a7d](http://192.168.100.56:3000/denozord/EvoBGP/commit/0cb5a7d))
* fix: update bird build script and Dockerfile to include readline dependency ([35ed79b](http://192.168.100.56:3000/denozord/EvoBGP/commit/35ed79b))
* fix: update bird build script and Dockerfile to remove readline dependency ([fefadac](http://192.168.100.56:3000/denozord/EvoBGP/commit/fefadac))
* fix: update deployment configuration for Traefik and secure Web UI ([00bd037](http://192.168.100.56:3000/denozord/EvoBGP/commit/00bd037))
* fix: update Docker build commands in CI configuration and documentation ([b5061a5](http://192.168.100.56:3000/denozord/EvoBGP/commit/b5061a5))
* fix(httpapi): phase 1 compliance — ERR-01, SEC-04, CDN client, scheduler docs ([1dd7135](http://192.168.100.56:3000/denozord/EvoBGP/commit/1dd7135))
* test: add CDN prefetch tests for 304 responses and snapshot handling ([a536a2d](http://192.168.100.56:3000/denozord/EvoBGP/commit/a536a2d))
* test: add validation for 'strict bind on' in BGP rendering tests. Enhance BGP test cases to ensure ' ([dc5e20e](http://192.168.100.56:3000/denozord/EvoBGP/commit/dc5e20e))
* docker: refactor cache handling in Docker bake configuration ([5e59a1c](http://192.168.100.56:3000/denozord/EvoBGP/commit/5e59a1c))
* docker: simplify web image targets in Docker bake configuration ([765a805](http://192.168.100.56:3000/denozord/EvoBGP/commit/765a805))
* docker: update web image build process and README ([92f6de8](http://192.168.100.56:3000/denozord/EvoBGP/commit/92f6de8))
* web(ui): app layout — sidebar, mobile sheet, topbar, PageHeader ([2d78c81](http://192.168.100.56:3000/denozord/EvoBGP/commit/2d78c81))
* web(ui): AppDataTable pattern and modules pilot ([b992301](http://192.168.100.56:3000/denozord/EvoBGP/commit/b992301))
* web(ui): design tokens — spacing and component scale ([ac635e9](http://192.168.100.56:3000/denozord/EvoBGP/commit/ac635e9))
* web(ui): feedback — confirm dialog, skeletons, README ([87309cf](http://192.168.100.56:3000/denozord/EvoBGP/commit/87309cf))
* web(ui): form patterns — FormField, formsnap, directories pilot ([77a2cfd](http://192.168.100.56:3000/denozord/EvoBGP/commit/77a2cfd))
* web(ui): foundation — ui/core layout, shadcn primitives, notify ([00a2a44](http://192.168.100.56:3000/denozord/EvoBGP/commit/00a2a44))
* web(ui): migrate dashboard, schedule, monitoring to ui patterns ([57dcf30](http://192.168.100.56:3000/denozord/EvoBGP/commit/57dcf30))
* web(ui): migrate network to ui patterns ([3a3056b](http://192.168.100.56:3000/denozord/EvoBGP/commit/3a3056b))
* web(ui): migrate operations and module detail to ui patterns ([1f9930d](http://192.168.100.56:3000/denozord/EvoBGP/commit/1f9930d))
* web(ui): migrate settings to ui patterns ([9808aca](http://192.168.100.56:3000/denozord/EvoBGP/commit/9808aca))
* Enhance EvoBGP architecture plan by refining the overview, updating todos with detailed descriptions ([30efdbf](http://192.168.100.56:3000/denozord/EvoBGP/commit/30efdbf))
* init ([cbf3aaa](http://192.168.100.56:3000/denozord/EvoBGP/commit/cbf3aaa))
* Refine EvoBGP architecture plan with updated database schema details, improved descriptions for comp ([431ef3e](http://192.168.100.56:3000/denozord/EvoBGP/commit/431ef3e))
* Update EvoBGP architecture plan with new overview and todos. Refined descriptions for control-plane  ([31ff5bb](http://192.168.100.56:3000/denozord/EvoBGP/commit/31ff5bb))
2026-05-19 17:58:24 +00:00
Denozordec bb334b10f5 feat: implement versioning and release management in the project
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 23s
CI / web (push) Successful in 28s
CI / go (push) Successful in 26s
CI / bird2 (push) Successful in 15s
CI / changes (push) Successful in 7s
- Added automatic versioning based on git tags using semantic-release in Gitea Actions.
- Introduced new API endpoints `GET /version` and `GET /v1/version` to expose build metadata.
- Updated Docker build process to include version and build time information in Go binaries.
- Enhanced documentation with details on release processes and versioning guidelines.
- Integrated version display in the web application for better user visibility.
2026-05-20 00:56:02 +07:00
Denozordec 4c23232c4e docs: update AGENTS and README to include guidelines for Conventional Commits
- Added a section on Conventional Commits in AGENTS.md, detailing the process for generating commit messages.
- Enhanced README.md with references to the Conventional Commits rules and the necessary scripts for generating commit messages.
- Clarified the format for commit messages, specifying the language requirements for headers and bodies.
2026-05-20 00:48:21 +07:00
Denozordec c263fd5c7e refactor: improve code consistency and formatting across components
CI / openapi (push) Has been skipped
CI / web (push) Successful in 29s
CI / go (push) Has been skipped
CI / docker-web (push) Successful in 1m22s
CI / docker-bird (push) Has been skipped
CI / docker-go (push) Has been skipped
CI / changes (push) Successful in 7s
CI / bird2 (push) Has been skipped
- Standardized formatting in various Svelte components for better readability.
- Updated README.md and .prettierrc for consistent styling and improved documentation.
- Refactored class attributes in UI components to enhance clarity and maintainability.
- Adjusted layout files to ensure proper alignment and spacing in the application interface.
2026-05-20 00:37:00 +07:00
Denozordec 6bee5adc34 feat: add optional name field to DoH profile schema and types
CI / openapi (push) Successful in 29s
CI / web (push) Failing after 45s
CI / go (push) Has been skipped
CI / docker-web (push) Successful in 1m37s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (push) Has been skipped
CI / changes (push) Successful in 10s
- Introduced an optional `name` property in the OpenAPI schema for DoH profiles, providing a display name for profiles.
- Updated TypeScript types to reflect the new optional `name` field in `DohProfile` and `DohProfileCreate` types.
2026-05-20 00:26:58 +07:00
DenozordecandCursor 64516c316d ci: phase 4 automation — web job, lint-httpapi, golangci
CI / openapi (push) Has been skipped
CI / web (push) Failing after 41s
CI / go (push) Successful in 52s
CI / docker-web (push) Successful in 2m14s
CI / docker-bird (push) Successful in 1m2s
CI / bird2 (push) Successful in 21s
CI / docker-go (push) Successful in 3m34s
CI / changes (push) Successful in 10s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 00:23:14 +07:00
DenozordecandCursor ea65451160 chore: phase 3 code quality — pipeline prefixes, godoc baseline
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 00:23:14 +07:00
DenozordecandCursor 250cdea35d refactor: phase 2 structural alignment — reports, importer, nodecli, pagination
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 00:22:39 +07:00
DenozordecandCursor 1dd713514c fix(httpapi): phase 1 compliance — ERR-01, SEC-04, CDN client, scheduler docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 00:21:16 +07:00
Denozordec aff27e8f7b docs: update AGENTS and README with engineering rules and guidelines
CI / openapi (push) Has been skipped
CI / go (push) Successful in 43s
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 21s
CI / docker-go (push) Successful in 3m41s
CI / changes (push) Successful in 11s
- Added engineering rules references in AGENTS.md for code changes and specific areas (web, BIRD).
- Enhanced README.md to include links to engineering rules for different development areas.
- Updated birdfmt documentation to specify engineering rules for BIRD/BGP/IP.
- Clarified UI development guidelines in web/README.md to follow shadcn-svelte documentation and repository rules.
2026-05-20 00:14:14 +07:00
Denozordec 07b97bddc1 feat: enhance observability with prefix aggregation metrics
CI / openapi (push) Has been skipped
CI / go (push) Successful in 33s
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m30s
CI / changes (push) Successful in 7s
- Added new histograms to track prefix aggregation duration, raw count, and aggregated count during tenant rendering.
- Implemented `RecordPrefixAggregation` function to record metrics for aggregation performance.
- Updated `RenderTenantRevision` and `RenderTenantRevisionFromPrefixes` functions to log aggregation statistics.
- Refactored `smartAggregatePrefixRows` to support both IPv4 and IPv6 aggregation, improving overall prefix handling.
2026-05-19 15:38:33 +07:00
Denozordec 34ecc5c235 feat: enhance DoH profile management and resolver policy in modules
CI / openapi (push) Successful in 1m2s
CI / go (push) Successful in 27s
CI / docker-web (push) Successful in 1m27s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m5s
CI / changes (push) Successful in 6s
- Added `DohResolverPolicy` schema to OpenAPI documentation, defining policies for domain resolution.
- Updated module handling to support multiple DoH profiles via `doh_profile_ids` and introduced `doh_resolver_policy` in the API.
- Refactored related functions to accommodate the new DoH profile structure, ensuring backward compatibility with existing `doh_profile_id`.
- Enhanced UI components to allow selection and management of DoH profiles and policies in the web interface.
- Updated database interactions to handle new fields and ensure proper data normalization.
2026-05-19 14:57:50 +07:00
Denozordec a536a2d5eb test: add CDN prefetch tests for 304 responses and snapshot handling
CI / openapi (push) Has been skipped
CI / go (push) Successful in 29s
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go (push) Successful in 8m32s
CI / changes (push) Successful in 8s
- Introduced `TestRefreshModuleIngest_CDN304UsesStoredSnapshot` to verify that the module ingest correctly uses stored snapshots when receiving a 304 Not Modified response.
- Added `TestCollectModulePrefixRows_CDN304RetriesWithoutETag` to ensure that the system retries without the If-None-Match header after a 304 response, and correctly collects prefixes on subsequent successful requests.
- Updated `cachedCDNPrefixRows` function to improve handling of cached prefixes during CDN source processing.
- Modified `RefreshModuleIngest` to utilize prior snapshots when collecting module prefix rows.
2026-05-19 14:26:49 +07:00
Denozordec 765a8052da docker: simplify web image targets in Docker bake configuration
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Successful in 1m20s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go (push) Successful in 7m50s
CI / changes (push) Successful in 8s
- Removed the "web-deps" and "web-build" targets from the web-images group to streamline the build process.
- Updated the configuration to focus on the primary targets "evobgp-web" and "evobgp-web-all".
2026-05-19 14:13:40 +07:00
Denozordec 5e59a1c334 docker: refactor cache handling in Docker bake configuration
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Failing after 15s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 14s
CI / docker-go (push) Successful in 2m36s
CI / changes (push) Successful in 7s
- Renamed cache functions for Go and web targets to clarify their purpose and prevent race conditions during parallel builds.
- Updated targets to use the new cache export functions, ensuring proper cache management.
- Revised README to reflect changes in cache usage and provide guidance on resolving potential CI issues related to cache schema changes.
2026-05-19 14:12:10 +07:00
Denozordec 92f6de8695 docker: update web image build process and README
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Failing after 16s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 22s
CI / docker-go (push) Failing after 51s
CI / changes (push) Successful in 8s
- Added a new target "web-build" to streamline the build process for web artifacts.
- Updated "evobgp-web" and "evobgp-web-all" targets to utilize the new "web-build" context.
- Modified Dockerfile to reflect changes in the build stages and context for web artifacts.
- Revised README to clarify the new build process and dependencies.
2026-05-19 13:37:32 +07:00
DenozordecandCursor 87309cfcec web(ui): feedback — confirm dialog, skeletons, README
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Failing after 1m21s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (push) Has been skipped
CI / changes (push) Successful in 7s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 1f9930d467 web(ui): migrate operations and module detail to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 9808aca44f web(ui): migrate settings to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 3a3056ba76 web(ui): migrate network to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 57dcf306f3 web(ui): migrate dashboard, schedule, monitoring to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:11 +07:00
DenozordecandCursor b992301c99 web(ui): AppDataTable pattern and modules pilot
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:59:34 +07:00
DenozordecandCursor 77a2cfd426 web(ui): form patterns — FormField, formsnap, directories pilot
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:59:25 +07:00
DenozordecandCursor ac635e91eb web(ui): design tokens — spacing and component scale
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:56:38 +07:00
DenozordecandCursor 2d78c8156f web(ui): app layout — sidebar, mobile sheet, topbar, PageHeader
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:56:24 +07:00
DenozordecandCursor 00a2a44630 web(ui): foundation — ui/core layout, shadcn primitives, notify
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:54:29 +07:00
Denozordec 35ed79bdd6 fix: update bird build script and Dockerfile to include readline dependency
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Successful in 40s
CI / bird2 (push) Successful in 14s
CI / docker-go (push) Successful in 2m50s
CI / changes (push) Successful in 6s
Modified the bird build script to enable readline support and updated the Dockerfile to install both libreadline8 and libncurses6, ensuring compatibility with the birdc runtime requirements.
2026-05-19 11:14:28 +07:00
Denozordec 0cb5a7d4cb fix: update bird build script and Dockerfile to include libncurses dependency
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Successful in 41s
CI / bird2 (push) Successful in 14s
CI / docker-go (push) Failing after 2m5s
CI / changes (push) Successful in 7s
Modified the bird build script to install libncurses-dev, ensuring compatibility with the updated build process. Updated the Dockerfile to replace the libreadline8 package with libncurses6, aligning with the new dependency requirements for birdc.
2026-05-19 11:10:50 +07:00
Denozordec fefadac1bf fix: update bird build script and Dockerfile to remove readline dependency
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Successful in 43s
CI / docker-go (push) Failing after 2m4s
CI / changes (push) Successful in 7s
CI / bird2 (push) Successful in 18s
Modified the bird build script to disable readline support, streamlining the installation process. Updated the Dockerfile to include the necessary libreadline8 package for the birdc runtime, ensuring compatibility while reducing unnecessary dependencies in the build phase.
2026-05-19 11:05:57 +07:00
Denozordec 7d52a4af63 refactor: update Docker build context and paths in CI configuration and documentation
CI / openapi (push) Has been skipped
CI / go (push) Successful in 29s
CI / docker-web (push) Successful in 1m30s
CI / docker-bird (push) Successful in 43s
CI / bird2 (push) Successful in 15s
CI / docker-go (push) Successful in 3m0s
CI / changes (push) Successful in 6s
Modified the CI workflow to include the `--allow=fs.read` flag for Docker build commands, allowing access to the repository context. Updated the `docker-bake.hcl` file to reflect the correct paths for Dockerfiles, ensuring consistency with the new structure. Adjusted the README to clarify the changes in build commands and paths, enhancing overall documentation accuracy.
2026-05-19 10:56:25 +07:00
Denozordec 9ab7de65ce chore: update CI configuration to disable filesystem entitlement for Docker builds
CI / openapi (push) Has been skipped
CI / go (push) Successful in 25s
CI / docker-web (push) Failing after 16s
CI / docker-bird (push) Failing after 16s
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Failing after 14s
CI / changes (push) Successful in 7s
Added the BUILDX_BAKE_ENTITLEMENTS_FS environment variable set to "0" in the CI workflow to allow Docker builds to read the repository context. Updated the README to clarify this change and provide guidance on local usage for similar issues.
2026-05-19 10:53:51 +07:00
Denozordec 6025f383f1 refactor: streamline Docker build process in CI configuration and documentation
CI / openapi (push) Has been skipped
CI / go (push) Successful in 22s
CI / docker-web (push) Failing after 14s
CI / docker-bird (push) Failing after 14s
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Failing after 15s
CI / changes (push) Successful in 6s
Updated the CI workflow to utilize a script for generating Docker build variable overrides, simplifying the build commands for Go and Web images. Adjusted the README to reflect the new build process and clarify the usage of environment variables, enhancing overall documentation clarity.
2026-05-19 10:51:55 +07:00
Denozordec 27e01d010f refactor: update Docker build variable syntax in CI configuration and documentation
CI / openapi (push) Has been skipped
CI / go (push) Successful in 22s
CI / docker-web (push) Failing after 14s
CI / docker-bird (push) Failing after 14s
CI / bird2 (push) Successful in 14s
CI / docker-go (push) Failing after 14s
CI / changes (push) Successful in 6s
Changed the variable assignment syntax in the CI workflow from `--set` to `--var` for Docker build commands. Updated the README to reflect this change and clarify the usage of variables in the docker-bake.hcl file.
2026-05-19 10:50:05 +07:00
Denozordec b5061a58d9 fix: update Docker build commands in CI configuration and documentation
CI / openapi (push) Has been skipped
CI / go (push) Successful in 21s
CI / docker-web (push) Failing after 13s
CI / docker-bird (push) Failing after 14s
CI / bird2 (push) Successful in 13s
CI / docker-go (push) Failing after 13s
CI / changes (push) Successful in 6s
Refactored the Docker build commands in the CI workflow to remove the wildcard prefix from variable settings, ensuring proper variable assignment. Updated the README documentation to reflect these changes and provide clearer instructions for building images with the correct variable syntax.
2026-05-19 10:48:09 +07:00
Denozordec 80229b33fc feat: update CI configuration and Dockerfiles for improved build efficiency
CI / openapi (push) Has been skipped
CI / go (push) Successful in 1m29s
CI / docker-web (push) Failing after 15s
CI / docker-bird (push) Failing after 14s
CI / bird2 (push) Successful in 14s
CI / docker-go (push) Failing after 14s
CI / changes (push) Successful in 7s
Enhanced the CI workflow by adding support for docker-bake.hcl to streamline the building and pushing of Go images. Updated Go version to 1.24 and introduced caching for Go module dependencies. Refactored Dockerfiles for evobgp-agent, evobgp-web, and gobinary to utilize build stages more effectively, improving caching and build performance. Adjusted installation commands and entry points for better clarity and maintainability.
2026-05-19 10:44:24 +07:00
Denozordec 7a2b015b12 feat: add tenant refresh functionality to HTTP API and job processing
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Successful in 59s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-go-prime (push) Successful in 55s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / bird2 (push) Successful in 43s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 11s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Implemented a new endpoint for tenant refresh in the HTTP API, allowing for the refresh of modules associated with a tenant. Enhanced job processing to handle tenant refresh jobs, including logic for managing module IDs and job status updates. Updated the scheduler to enqueue tenant refresh jobs based on module due dates, improving the overall efficiency of module management. Additionally, introduced caching for ASN prefix data to optimize performance during refresh operations.
2026-05-19 10:35:20 +07:00
Denozordec ee364c8b6d feat: update collectModulePrefixRows to support prior snapshots and enhance CDN/domain prefix collection
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / go (push) Successful in 1m56s
CI / docker-go-prime (push) Successful in 26s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m23s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m15s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m32s
CI / changes (push) Successful in 7s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m40s
Modified the collectModulePrefixRows function to accept an optional priorSnapshot parameter, allowing for more efficient data retrieval by skipping unnecessary CDN fetches. Refactored the logic for collecting prefix rows from AS, CDN, and domain sources to utilize dedicated functions, improving code organization and maintainability. Additionally, introduced caching for module prefix snapshots to optimize performance during refresh operations.
2026-05-19 10:22:33 +07:00
Denozordec 1a61ae2d71 feat: add optional name field to BGP peer creation and update UI
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m22s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m27s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 15s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Introduced an optional 'name' field in the BgpPeerCreate type to enhance peer identification. Updated the peer creation and editing forms to include this new field, improving user experience. Adjusted the UI to display the peer name in the peer listing table, ensuring better visibility of peer information.
2026-04-22 15:02:14 +07:00
Denozordec 458a885868 feat: enhance monitoring page with detailed status tracking and error handling
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m9s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Updated the monitoring page to include comprehensive tracking of API health, BGP sessions, and job statuses. Introduced new state variables for error handling and last updated timestamps. Enhanced the UI to display overall system status, including detailed hints for potential issues, improving incident diagnosis capabilities. Added functions to summarize job statuses and derive overall health indicators, ensuring better visibility into system performance.
2026-04-09 16:42:37 +07:00
Denozordec 1e13ef9e70 feat: add last refreshed timestamp to modules and CDN sources
CI / openapi (push) Successful in 23s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m32s
CI / go (push) Successful in 45s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m28s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m8s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m14s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m2s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 6m21s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m26s
Introduced a new field, last_refreshed_at, to track the last successful update time for modules and CDN sources. Updated the database schema, API responses, and internal logic to support this feature. Enhanced the frontend to display the last refreshed timestamp, improving visibility into the data update status for users.
2026-04-09 16:10:00 +07:00
Denozordec 47764345f6 feat: enhance CDN source management with last refreshed timestamp
Added functionality to track the last refreshed timestamp for CDN sources. Updated the database schema and relevant methods to include the last refreshed timestamp during creation and updates. Implemented logic to skip fetching CDN sources based on their refresh interval, improving efficiency in the module prefix collection process. Enhanced the data retrieval methods to support the new timestamp field, ensuring accurate state management for CDN sources.
2026-04-09 16:04:53 +07:00
Denozordec 8a50ba44b3 feat: add live BGP protocol state tracking to peer listing
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m28s
CI / go (push) Successful in 39s
CI / docker-go-prime (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 6m19s
CI / bird2 (push) Successful in 13s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m25s
Enhanced the peer listing API to include live BGP session states by integrating a new function to retrieve protocol states from the BIRD socket. Added parsing logic for BGP protocol states and updated the peer JSON response to include session state information. Introduced unit tests for the new parsing functionality to ensure accuracy in state retrieval.
2026-04-09 15:39:01 +07:00
Denozordec 6107c3eb87 refactor: enhance job status checks in worker tests
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 39s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 6m8s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
Updated the worker test cases to improve the handling of job status checks. Added nil checks for job instances and refined the logic to verify job statuses directly from snapshots, ensuring more robust test coverage and preventing potential nil pointer dereferences.
2026-04-09 15:14:19 +07:00
Denozordec 3b17228ef2 feat: implement peer reconciliation job for BGP peers
CI / openapi (push) Successful in 23s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Failing after 30s
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Added a new job type `peer_reconcile` to handle fast reconciliation of BGP peers without module ingestion. Updated API documentation to reflect the new job's functionality and its automatic application to speakers post-reconciliation. Enhanced the HTTP API to enqueue reconciliation jobs during peer creation, updates, and deletions, ensuring efficient state management for BGP configurations.
2026-04-09 15:07:21 +07:00
Denozordec e700f90c47 refactor: improve PATCH handling for module updates with nullable field support
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m30s
CI / go (push) Successful in 47s
CI / docker-go-prime (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m27s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m11s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m44s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m4s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / changes (push) Successful in 7s
CI / bird2 (push) Successful in 17s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m31s
Enhanced the handlePatchModule function to read the request body more robustly and handle nullable fields explicitly. This allows clients to differentiate between omitted fields and fields set to null, improving data integrity during module updates. The changes streamline the JSON unmarshalling process and ensure proper handling of nullable values for community and DoH profile selections.
2026-04-08 21:08:09 +07:00
Denozordec 1765ab89e7 refactor: streamline edit form handling for community and DoH profile selections
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m9s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Removed unnecessary state variables for community and DoH profile selections in the edit form. Updated the handling of these selections to directly utilize values from the edit form, improving code clarity and reducing redundancy. This change enhances the overall user experience by simplifying the state management in the module edit process.
2026-04-08 20:57:29 +07:00
Denozordec b99fc179ab refactor: enhance module interval label logic and add reset buttons in edit forms
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Refined the moduleIntervalLabel function to improve handling of cron expressions and refresh intervals, ensuring clearer display of module timing information. Added reset buttons for community and DoH profile selections in the edit forms, enhancing user experience by allowing quick resets to default values.
2026-04-08 19:39:17 +07:00
Denozordec 1f85654653 refactor: consolidate module interval display logic and improve edit form handling
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Introduced a new utility function, moduleIntervalLabel, to streamline the display of module interval information across different components. Enhanced the edit form functionality by ensuring proper handling of nullable values for community and DoH profile selections, improving data integrity and user experience during module updates.
2026-04-08 19:33:09 +07:00
Denozordec 05ec176c7a refactor: improve module save functionality and nullable value handling
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Updated the saveMod function to ensure the module is reloaded after a successful update, enhancing data consistency. Refined the nullableSelectValue and fromNullableSelect functions to handle empty string cases, improving input validation and user experience in module pages.
2026-04-08 19:23:27 +07:00
Denozordec 25374a6919 refactor: enhance module display and input handling in module pages
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Updated the module display logic to improve the handling of nullable values for refresh intervals and community selections. Introduced new utility functions for managing nullable select values, ensuring better user experience and data integrity. Additionally, enhanced the visual presentation of module metadata with updated icons and styling, improving overall clarity and usability in the module pages.
2026-04-08 16:38:22 +07:00
Denozordec dc1f4a59b0 refactor: update job kind display logic in schedule page
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m8s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m7s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Replaced the job kind filter with a new function to improve the display of job kinds in the schedule page. Introduced a derived map for module names to enhance job information presentation. This change streamlines the rendering of job details, improving clarity and user experience.
2026-04-08 15:59:17 +07:00
Denozordec ae9fbe30b4 feat: add auto-updater service for Docker stack with configurable settings
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Introduced an auto-updater service in the Docker Compose configuration to enable automatic updates for specified services without restarting protected services like BIRD2. Configurable settings include update interval, enabled status, and a list of services to update, enhancing deployment efficiency. Updated documentation to reflect new features and usage instructions.
2026-04-08 15:52:49 +07:00
Denozordec 9f021289c0 fix: improve sidebar and navigation layout in AppShell component
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Updated the AppShell component to enhance the sidebar's layout by making it sticky and ensuring it occupies the full height of the screen. Additionally, modified the main navigation to allow for vertical scrolling, improving usability and accessibility for users navigating through the application.
2026-04-08 15:44:30 +07:00
Denozordec 40c8a8d38a feat: implement countAll function for API data retrieval
Added a new `countAll` function to streamline the retrieval of item counts from various API endpoints. This function replaces individual API calls with a unified approach, enhancing performance and code clarity. Updated the onMount lifecycle method to utilize this new function for fetching counts of modules, revisions, peers, and speakers, improving data handling efficiency.
2026-04-08 15:42:50 +07:00
Denozordec d91667f698 feat: enhance settings page layout and section management
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Redesigned the settings page to improve user experience by introducing a section-based layout for managing API keys and BIRD parameters. Added a new type for settings sections and implemented dynamic content rendering based on the selected section. Updated descriptions for clarity and improved the overall structure of the settings interface.
2026-04-08 14:46:56 +07:00
Denozordec 1386b63db2 fix: improve input handling and validation for settings fields
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m2s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Updated the settings page to ensure proper handling of potentially undefined values by using the nullish coalescing operator. This change enhances the validation logic for various input fields, ensuring that only valid IPv4 and IPv6 addresses, as well as positive integers, are accepted. Additionally, improved user feedback by adding an error message when attempting to save settings with validation errors.
2026-04-08 14:33:43 +07:00
Denozordec 4f50350990 feat: implement revision retention settings and pruning logic
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m24s
CI / go (push) Successful in 44s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
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 2m12s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m19s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m21s
Added functionality to handle revision retention minutes in global settings, including validation for input values. Introduced a new method to prune old revisions based on the specified retention period, ensuring that only the most recent revisions are kept. Updated the frontend to allow users to configure the revision retention setting, enhancing the management of revision lifecycles.
2026-04-08 14:08:23 +07:00
Denozordec 10e2415cfa refactor: streamline module refresh process and enhance revision handling
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m26s
CI / go (push) Successful in 40s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m9s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m11s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m25s
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
Updated the worker's job processing to utilize a new `RefreshModuleIngest` function, which simplifies the module refresh logic by separating ingestion from revision rendering. The `finishModuleRefreshSuccess` method now handles the aggregation of revision logs and enqueues deploy actions more efficiently. Additionally, refactored the `RefreshModule` function to maintain backward compatibility while improving clarity and functionality in the pipeline's revision management.
2026-04-08 13:42:53 +07:00
Denozordec 2260ecf74a feat: add diagnostic log endpoint for revisions and implement download functionality
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 38s
CI / docker-go-prime (push) Successful in 22s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 3m10s
CI / bird2 (push) Successful in 18s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m20s
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m20s
Introduced a new endpoint `GET /revisions/{revision_id}/diagnostic-log` to retrieve detailed diagnostic logs for specific revisions. Implemented the corresponding handler to generate and return the log in a structured format. Additionally, updated the frontend to include a download button for the diagnostic log, enhancing user accessibility to revision data.
2026-04-08 13:12:30 +07:00
Denozordec 6d0e214655 feat: add debug logging and optimize database queries in Postgres repository
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 40s
CI / docker-go-prime (push) Successful in 25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m3s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 3m4s
CI / bird2 (push) Successful in 17s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
Introduced a new debug logging function to capture detailed information during specific operations, controlled by an environment variable. Optimized the MaterializedPrefixStats and ListRevisions methods to reduce memory usage by leveraging SQL aggregation and limiting result sets. Updated the ListRevisionPrefixes method to include pagination support, enhancing performance and efficiency in data retrieval.
2026-04-08 12:51:15 +07:00
Denozordec f6b94a44d0 refactor: enhance bird metrics polling with context support
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / go (push) Successful in 40s
CI / docker-go-prime (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m24s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m9s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m18s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m20s
CI / changes (push) Successful in 8s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
Updated the `startBirdMetricsPoller` function to accept a context parameter, allowing for better control over the polling lifecycle. This change was applied in both `evobgp-all` and `evobgp-api` main files. Additionally, modified the `StartBirdProtocolsPoller` function to handle context cancellation, ensuring graceful shutdown of the polling routine. Introduced a new service in the Docker Compose configuration for logging runtime service outputs, improving observability during deployment.
2026-04-08 12:29:07 +07:00
Denozordec 39ba976454 feat: update Traefik deployment configuration for Let's Encrypt and logging
Enhanced the Traefik configuration in the docker-compose file by specifying the volume name for Let's Encrypt certificates to ensure proper storage and retrieval. Added a logging level setting for improved visibility during operation. Updated comments for clarity on usage and backup procedures.
2026-04-08 12:01:50 +07:00
Denozordec 9163aeca1e Revert "feat: add router-lists catalog API client helper"
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
This reverts commit 6c8e7c55ad.
2026-04-07 23:21:17 +07:00
Denozordec 6c8e7c55ad feat: add router-lists catalog API client helper
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m6s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Expose a typed helper for /v1/router-lists/catalog so router-lists-ui can consume modules, entries, and communities in one request.

Made-with: Cursor
2026-04-07 23:17:40 +07:00
Denozordec c811c43bbc feat: add aggregated router-lists catalog endpoint and update module listing filters
CI / openapi (push) Successful in 22s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / go (push) Successful in 38s
CI / docker-go-prime (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / bird2 (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
Introduced a new endpoint `GET /v1/router-lists/catalog` that returns a consolidated view of modules, domain entries, ASNs, IP ranges, and communities. Enhanced the existing module listing functionality to support filtering by type and enabled status. Updated documentation to reflect these changes and added tests for the new endpoint and filtering capabilities.
2026-04-07 23:15:40 +07:00
Denozordec b7a8aab2e8 docs: add router-lists-ui integration documentation to README
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Included a new section in the README to document the integration of `router-lists-ui` with the EvoBGP API, covering key components such as DOMAINS, IP_RANGES, AS_PREFIXES, and communities. This enhances the clarity and usability of the documentation for users.
2026-04-07 22:50:32 +07:00
Denozordec 00bd03735a fix: update deployment configuration for Traefik and secure Web UI
CI / openapi (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Changed the Traefik image version to 'latest' in both docker-compose files and updated the environment variables in the .env file for the secure Web UI, including new domain and IP whitelist settings. This ensures compatibility and security enhancements for the deployment.
2026-04-07 22:35:54 +07:00
Denozordec 4acd6857c9 feat: enhance deployment configuration for secure Web UI with Traefik integration
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
Added support for a secure Web UI using Traefik with Let's Encrypt and Cloudflare DNS challenge. Updated docker-compose files to include new service definitions and environment variables for configuration. Improved documentation to guide users in setting up the secure UI environment.
2026-04-07 21:29:19 +07:00
Denozordec 1e77e1a2b8 feat: auto-refresh non-IP module updates and add full project manual
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m21s
CI / go (push) Successful in 38s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m5s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
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 / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m18s
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
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
2026-04-07 21:09:38 +07:00
Denozordec f877ec89a7 feat: improve peer speaker assignment UX in network settings
Replace manual speaker id input with a validated speaker selector and clearer speaker labels so operators can configure peers with fewer input mistakes and better context.

Made-with: Cursor
2026-04-07 21:08:00 +07:00
Denozordec 2da6d49368 feat: redesign settings page with structured controls
Replace raw JSON editing on the settings page with validated BIRD-specific fields and a guided key/value section so operators can update configuration safely without JSON syntax errors.

Made-with: Cursor
2026-04-07 21:04:40 +07:00
Denozordec 53fd527a6d feat: enhance community label handling and improve UI localization across operations pages. Introduce community label mapping for better readability in job logs and update various components to reflect Russian translations, enhancing user experience and consistency in the interface.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / go (push) Successful in 43s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m30s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m9s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m15s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m5s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m29s
CI / bird2 (push) Successful in 17s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
2026-04-07 00:58:15 +07:00
Denozordec e1c4cbce93 feat: enhance job filtering and display functionality in operations page. Introduce job search and filter options, allowing users to refine job listings by status, kind, and module. Update OperationsJobsTab to reflect total fetched jobs and improve overall user experience with better layout and responsiveness.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-07 00:41:29 +07:00
Denozordec 4aaaa30b1b feat: update RevisionDiff type and enhance OperationsDiffTab component for better diff handling. Introduce optional prefixes structure in RevisionDiff and refactor diffAdded/diffRemoved functions to utilize new data structure, improving clarity and functionality in the UI.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-07 00:26:23 +07:00
Denozordec d83efe24ef refactor: optimize job listing and counting logic for concurrency safety. Separate tenant filtering and status checks to reduce lock contention, improving performance and ensuring accurate job status retrieval.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m49s
CI / go (push) Successful in 1m12s
CI / docker-go-prime (push) Successful in 29s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m14s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m26s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m30s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m54s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m19s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m47s
CI / changes (push) Successful in 14s
CI / bird2 (push) Successful in 20s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m45s
2026-04-07 00:03:46 +07:00
Denozordec 149fb0a18c feat: implement module refresh job management with concurrency control. Add CountOtherActiveModuleRefresh method to track active jobs and enhance finishModuleRefreshSuccess to defer deploy_apply when parallel refreshes are detected, improving job processing efficiency.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Failing after 42s
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 23:59:26 +07:00
Denozordec b140dcbcf2 feat: add @tanstack/table-core dependency and integrate module name mapping in operations page. Update OperationsJobsTab to utilize new job kind title functionality and enhance job detail presentation with improved layout and overflow handling.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 43s
CI / docker-go-prime (push) Successful in 23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m5s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m5s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m29s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m2s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m12s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m18s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m21s
2026-04-06 23:45:19 +07:00
Denozordec b541794d84 feat: enhance community attribute handling in BIRD format. Add validation for community route bodies to ensure correct tuple structure and introduce tests for single-element standard arrays, improving error handling and robustness in community attribute processing. 2026-04-06 23:33:54 +07:00
Denozordec 0ca5542f6b feat: improve UI components with enhanced overflow handling and layout adjustments. Update card and table components to manage overflow effectively, ensuring better readability and user experience in the operations tabs.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 59s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 23:12:17 +07:00
Denozordec 6a7284245f feat: enhance UI components with improved styling and layout adjustments. Update button sizes, text classes, and overflow handling in various components for better readability and user experience across the operations page.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 59s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 22:52:37 +07:00
Denozordec 67ce135b46 feat: enhance job detail UI with improved overflow handling and layout adjustments. Update card components to manage overflow and ensure better readability of job reports and details, enhancing the overall user experience. 2026-04-06 22:36:23 +07:00
Denozordec 88226f6dd9 feat: refactor operations page to enhance UI and component structure. Remove unused card components and integrate OperationsQuickActions for improved quick action handling. Update tab layout for better responsiveness and user experience.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m2s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 22:29:32 +07:00
Denozordec 57c09acb3d feat: refine job detail UI with improved layout and scrolling capabilities. Enhance spacing and overflow handling for log entries and error messages, ensuring better readability and user experience. Integrate ScrollArea for log entries to manage overflow effectively.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 21:34:03 +07:00
Denozordec 97908b50c8 feat: improve job detail UI with enhanced spacing and layout adjustments. Update table cell classes for better text alignment and padding, ensuring a more consistent and readable presentation of job details and reports.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 12s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 21:18:53 +07:00
Denozordec 9f0b1281f2 feat: enhance API client with pagination and new data fetching functions. Implement apiPageAll for paginated API requests, and add functions to fetch all revision prefixes and module source catalogs based on module type. Update type definitions to support new response structures, improving data handling in the application.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m12s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m11s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 20:34:49 +07:00
Denozordec 1c5a9c91bb feat: refactor materialization logic to group static routes by community in the refresh pipeline. Introduce a new structure for managing static routes, enhancing the handling of IPv4 and IPv6 routes while preserving community associations. Update related functions to support the new grouping mechanism.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 38s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m8s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
2026-04-06 20:20:52 +07:00
Denozordec 0428b83eb5 feat: add smart aggregation for IPv4 CIDR prefixes in refresh pipeline. Implement functionality to aggregate prefixes based on community and source attributes, ensuring preservation of BIRD semantics. Enhance test coverage for aggregation logic and module refresh behavior.
CI / openapi (push) Has been skipped
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / go (push) Successful in 38s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m27s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m13s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 19:53:17 +07:00
Denozordec e816d175ea feat: implement bulk selection and deletion for modules and entries. Add functionality for selecting multiple modules, AS entries, CDN sources, domain entries, and IP ranges, along with a confirmation dialog for bulk deletion. Enhance UI to reflect selected counts and improve user experience in managing entries.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m2s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 5s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 19:18:52 +07:00
Denozordec bae2d68803 feat: add CSV import and export functionality for module entries. Implement endpoints for exporting and importing module entries in CSV format, supporting types AS_PREFIXES, DOMAINS, and IP_RANGES. Enhance UI with buttons for CSV operations, improving user experience in managing module data.
CI / openapi (push) Successful in 22s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 50s
CI / docker-go-prime (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m5s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m7s
CI / bird2 (push) Successful in 15s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 18:58:52 +07:00
Denozordec 233f29b437 feat: improve UI layout and responsiveness in module and operations pages. Refactor component structures for better alignment and spacing, ensuring a more consistent user experience across different screen sizes. Enhance button placements and text wrapping for improved readability. 2026-04-06 18:48:20 +07:00
Denozordec 4ba5b03f0b chore: upgrade Go version to 1.24 in Dockerfiles for evobgp-agent and gobinary. This change ensures compatibility with the latest features and improvements in the Go ecosystem.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Has been skipped
CI / docker-go-prime (push) Successful in 1m27s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m11s
CI / bird2 (push) Successful in 2m34s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m18s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
2026-04-06 18:08:52 +07:00
Denozordec 655d7507e5 feat: update Go module dependencies and enhance DNS resolution in refresh pipeline. Upgrade Go version to 1.24.0, add support for DNS over HTTPS using RFC8484 format, and improve error handling for domain resolution. Update UI components for better log entry presentation in Svelte.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Successful in 1m36s
CI / docker-go-prime (push) Failing after 35s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 17:59:52 +07:00
Denozordec 05bc5c435e feat: enhance job detail handling in UI with caching and loading states. Introduce SvelteMap for job details and SvelteSet for loading states, improving performance and user experience when expanding job entries. Update UI to reflect detailed job information dynamically.
CI / openapi (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m10s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 17:41:40 +07:00
Denozordec c48cc4e2d4 feat: enhance job processing with detailed logging and UI improvements. Introduce revision log entries in the Worker to capture job metadata, including source and community details. Update the Svelte UI to display job logs with expandable details, improving user experience and job tracking capabilities.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 27s
CI / docker-go-prime (push) Successful in 23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m3s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m16s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m15s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m7s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m13s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 7s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 17:10:29 +07:00
Denozordec 678bd27085 feat: implement domain resolution via DoH in refresh pipeline. Enhance the collectModulePrefixRows function to resolve domain IPs using DNS over HTTPS (DoH) profiles, adding support for both A and AAAA record types. Introduce new helper functions for handling DoH requests and processing responses, improving domain management and IP prefix aggregation.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / go (push) Successful in 25s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m7s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
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 2m13s
CI / bird2 (push) Successful in 17s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
2026-04-06 16:59:59 +07:00
Denozordec 42d956d109 feat: enhance worker functionality by adding job queuing for deploy_apply. Introduce a Registry field in the Worker struct and implement enqueueDeployAllSpeakers method to streamline job processing after refresh or rollback operations, improving deployment efficiency.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / go (push) Successful in 25s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m5s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m3s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m17s
CI / bird2 (push) Failing after 12s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m20s
2026-04-06 16:32:23 +07:00
Denozordec e20c9f3113 feat: implement CDN source preview functionality and enhance data handling. Add a new endpoint for previewing CDN sources, allowing users to fetch and parse CIDR prefixes from specified URLs. Update OpenAPI documentation to include new request and response schemas, and modify internal logic to support JSON parsing with prefix path traversal. Enhance UI components to accommodate new preview features, improving user experience and data management.
CI / openapi (push) Successful in 25s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m16s
CI / go (push) Successful in 29s
CI / docker-go-prime (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m15s
CI / docker-bird (push) Successful in 42s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m2s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m15s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m0s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 58s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m14s
CI / changes (push) Successful in 6s
CI / bird2 (push) Successful in 14s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m17s
2026-04-06 15:08:17 +07:00
Denozordec a2cbfacd45 feat: update BGP community structure and related documentation. Refactor BGP community handling by renaming fields from 'name' and 'kind' to 'community' and 'title' across the codebase. Update OpenAPI specifications, database interactions, and UI components to reflect these changes, enhancing clarity and consistency in community management.
CI / openapi (push) Successful in 25s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / go (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
2026-04-06 14:52:29 +07:00
Denozordec 482eeb122d refactor: remove strict bind configuration from BGP templates and update related logic. Eliminate 'strict bind on' from both IPv4 and IPv6 templates to simplify BGP peer rendering. Adjust OpenAPI documentation and internal handling of BGP source addresses to reflect these changes, enhancing clarity and compatibility.
CI / openapi (push) Successful in 23s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 2m27s
CI / go (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m9s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m7s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 54s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m19s
CI / bird2 (push) Successful in 55s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 7s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m26s
2026-04-06 14:03:25 +07:00
Denozordec dc5e20e9b2 test: add validation for 'strict bind on' in BGP rendering tests. Enhance BGP test cases to ensure 'strict bind on' is included in the rendered output, improving test coverage for BGP template configurations.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / go (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 55s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / changes (push) Successful in 6s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
2026-04-06 14:00:41 +07:00
Denozordec 20e674c1f2 fix: adjust Postgres resource limits and healthcheck parameters. Increase memory limit to 256M and modify healthcheck retries and start period to improve stability. Add notes on shared memory requirements and healthcheck behavior in Docker configuration. 2026-04-06 13:50:48 +07:00
Denozordec 07f2ce562e feat: add strict bind configuration to BGP templates. Enhance BGP template rendering by including 'strict bind on' for both IPv4 and IPv6 templates, improving address binding behavior in Docker and bridge environments.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 3m46s
CI / go (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m17s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m12s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m15s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 56s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m17s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
2026-04-06 13:32:23 +07:00
Denozordec 0b3ceaa7b8 refactor: switch BIRD installation from source to Ubuntu package. Update CI workflow and Dockerfiles for evobgp-agent and bird2 to install bird2 from the Ubuntu repository, improving compatibility and simplifying the build process.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 2m36s
CI / go (push) Successful in 21s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Failing after 47s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-bird (push) Successful in 40s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 3m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m20s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m8s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m3s
CI / bird2 (push) Successful in 14s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 2m46s
CI / changes (push) Successful in 8s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m20s
2026-04-06 12:53:46 +07:00
Denozordec e2002ba33d feat: update BIRD installation process to build from source. Modify CI workflow to ensure BIRD 2.14 is compiled from source instead of using the Debian package. Update Dockerfiles for evobgp-agent and gobinary to include BIRD 2.14 build steps, enhancing compatibility and performance.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-bird (push) Successful in 1m56s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 2m19s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / bird2 (push) Successful in 59s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
2026-04-06 12:44:31 +07:00
Denozordec 133ea93e83 feat: update BGP template configuration and OpenAPI documentation. Change import settings from 'all' to 'none' in BGP templates, and add hold and keepalive time parameters. Enhance OpenAPI description for bird_bgp_source_ipv4 to provide additional context for MikroTik configuration.
CI / openapi (push) Successful in 27s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m28s
CI / go (push) Successful in 26s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m29s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m4s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m25s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
2026-04-06 12:37:15 +07:00
Denozordec 6266a39aed feat: refine BGP template handling and documentation updates. Remove local IP requirements in BGP template options, simplifying the configuration to focus on ASN. Update OpenAPI documentation to clarify BIRD parameters and enhance user guidance on JSON settings format in the UI. Adjust tests to align with the new BGP template structure.
CI / openapi (push) Successful in 28s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m35s
CI / go (push) Successful in 25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m21s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m19s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m4s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m31s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 11:51:47 +07:00
Denozordec 62bf987a19 feat: update OpenAPI documentation and internal logic for BIRD settings. Enhance the description of BIRD parameters in openapi.yaml, adding details for optional BGP source addresses. Modify refresh.go to include handling for new BGP source address fields and update peer rendering logic accordingly. Improve user interface documentation in settings page to reflect changes in BIRD configuration options.
CI / openapi (push) Successful in 25s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m27s
CI / go (push) Successful in 22s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m13s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m35s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m3s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m26s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 11:35:36 +07:00
Denozordec 431c6a90f1 feat: enhance RefreshModule to aggregate prefixes from all enabled tenant modules. Update documentation to clarify the behavior of ingest and revision generation, ensuring accurate representation of data sources and prefix handling.
CI / openapi (push) Successful in 28s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / go (push) Successful in 22s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m29s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m22s
CI / bird2 (push) Successful in 14s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m18s
2026-04-06 11:03:15 +07:00
Denozordec be8e7b5742 feat: enhance BGP template and peer configuration handling. Introduce policies_json parameter for local IP and ASN overrides in BGP speakers. Implement BGP template rendering and update peer configuration to utilize templates, improving flexibility in BGP setup. Add tests for new rendering logic and ensure proper integration with existing configurations.
CI / openapi (push) Successful in 27s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m33s
CI / go (push) Successful in 23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m26s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / changes (push) Successful in 6s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
2026-04-06 10:18:51 +07:00
Denozordec c958d5af0d feat: implement AS holder name resolution and enhance ASEntry structure. Add ASHolderName function to retrieve organization names from RIPEstat, update ASEntry model to include ASN name, prefix count, and resolution timestamp. Modify database interactions and API responses to support new fields, improving ASN metadata handling.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m25s
CI / go (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m21s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m31s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m37s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m29s
CI / changes (push) Successful in 5s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m25s
2026-04-06 01:49:31 +07:00
Denozordec 80965adc0c feat: add enabled state to BGP peer types and UI. Implement peer enabling toggle in the network page, allowing users to enable or disable peers during creation and editing. Update relevant API calls and UI components to reflect the new functionality.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m0s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 01:35:13 +07:00
Denozordec 3e8543177e feat: implement effective peer enabling logic during peer creation. Update CreatePeer methods in Postgres and Memory stores to utilize EffectivePeerEnabledOnCreate function, ensuring new peers are enabled by default unless specified otherwise. Add tests for effective enabling behavior.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m28s
CI / go (push) Successful in 25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
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 1m24s
CI / bird2 (push) Successful in 14s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
2026-04-06 01:22:09 +07:00
Denozordec 80f3a8d105 feat: enhance peer neighbor handling by introducing NormalizePeerNeighborString function. Update CreatePeer and UpdatePeer methods in Postgres and Memory stores to validate and normalize neighbor input, improving data integrity and error handling.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m29s
CI / go (push) Successful in 26s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m19s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m22s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m29s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m4s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m26s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
2026-04-06 01:01:40 +07:00
Denozordec d92a06c9c8 feat: add @types/node and undici-types to devDependencies in package.json and package-lock.json for improved TypeScript support and HTTP/1.1 client functionality.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 5s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-06 00:42:58 +07:00
Denozordec 399622cc70 feat: improve BGP protocol summary handling by introducing isBGPProtocolSummaryRow function. Update SummarizeProtocolsOutput and related tests to ensure evobgp_* static names are not counted as BGP sessions, enhancing accuracy in protocol summaries.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / go (push) Successful in 23s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / bird2 (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
2026-04-06 00:41:59 +07:00
Denozordec 94a4c6acd4 feat: add BIRD status endpoint and integrate birdc checks into job processing. Enhance UI to display BIRD runtime status and job metadata, improving user feedback on BIRD operations.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m30s
CI / go (push) Successful in 25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m5s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m13s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m23s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m32s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m7s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m25s
CI / bird2 (push) Successful in 16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m30s
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
2026-04-06 00:25:43 +07:00
Denozordec e1aaa18377 feat: enhance RefreshModule to support legacy ASN resolution and improve prefix handling. Introduce sorting of ASNs and manage announced prefixes with polite pauses for better API interaction. Update buildPreviewFragments to streamline route rendering for BIRD configurations.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m49s
CI / go (push) Successful in 35s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m16s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m23s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m15s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m27s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m27s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m48s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m17s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m28s
CI / changes (push) Successful in 10s
CI / bird2 (push) Successful in 19s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m35s
2026-04-06 00:01:16 +07:00
Denozordec 1969a00151 feat: add bird configuration preview functionality with tabbed interface for fragments and prefixes. Implement fragment extraction and default path selection for improved user experience in previewing BIRD configurations.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 6s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-05 23:47:15 +07:00
Denozordec 693227163a refactor: improve handling of deployable fragments in ApplyRevision and buildPreviewFragments functions. Introduce isDeployableBirdFragment to filter out UI-only preview keys, and update related logic in the API to ensure proper job queuing and response handling for module refresh operations.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / go (push) Successful in 21s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m17s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m31s
CI / bird2 (push) Successful in 15s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m35s
2026-04-05 23:33:56 +07:00
Denozordec 4a860a088e refactor: enhance JSON struct tags for various data models in backend and memory store. Update fields in ModulePatch, CDNSource, ASEntry, DomainEntry, IPRangeEntry, DohProfile, Community, and BGPPeer to improve API response formatting and consistency.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m24s
CI / go (push) Successful in 24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m16s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m27s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m2s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m35s
CI / bird2 (push) Successful in 13s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m29s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m25s
2026-04-05 23:13:51 +07:00
Denozordec d4282c270b refactor: update Switch component styles and structure for improved accessibility and responsiveness. Adjust class bindings and enhance thumb positioning based on size prop.
CI / openapi (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m6s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / changes (push) Successful in 5s
CI / bird2 (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
2026-04-05 23:02:47 +07:00
Denozordec b70a643016 feat: implement theme selection and automatic theme application based on user preference. Update AppShell to include a dropdown for theme options and enhance layout to manage theme state. Adjust HTML language attribute to Russian and improve UI elements for better accessibility.
CI / openapi (push) Successful in 24s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m20s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m34s
CI / docker-bird (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
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 1m30s
CI / bird2 (push) Successful in 14s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m22s
CI / changes (push) Successful in 5s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m28s
2026-04-05 22:43:43 +07:00
Denozordec 1db6b5b7b2 refactor: update AS entry handling in API and database to support ASN-only entries. Remove prefix field from AS entry structure and adjust related functions and tests. Enhance OpenAPI specifications and documentation to reflect changes in AS entry representation. 2026-04-05 22:38:17 +07:00
425 changed files with 30880 additions and 4468 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
+113
View File
@@ -0,0 +1,113 @@
---
description: >-
Conventional Commits EvoBGP. Триггеры commit/коммит/закоммить/staged/commit message —
ОБЯЗАТЕЛЬНО сначала Shell scripts/commit/staged-context.ps1, скилл commit-message.
alwaysApply: false
---
# Conventional Commits (EvoBGP)
Полное описание релизного пайплайна: [docs/releasing.md](../../docs/releasing.md).
## Триггеры (применить правило + скилл)
Любой запрос на коммит или сообщение коммита: `commit`, `коммит`, `закоммить`, `git commit`, `commit message`, `conventional commit`, `staged`, «сгенерируй коммит» — в т.ч. если это указано в плане или [AGENTS.md](../../AGENTS.md).
## Обязательный запуск скрипта (MUST)
1. Прочитать скилл [`.cursor/skills/commit-message/SKILL.md`](../skills/commit-message/SKILL.md).
2. **Первым действием** выполнить Shell (из корня репо):
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
3. Сообщение коммита строить **только** по JSON из stdout скрипта (`groups`, `diff_excerpt`, `stat`).
4. **Запрещено** генерировать commit message без успешного (exit 0) запуска скрипта; не заменять скрипт одним `git diff --cached`.
## Формат (строго)
```
<type>(<scope>): <short summary in English>
<тело на русском: что изменено и зачем>
```
- **Заголовок** — только английский; императив, без точки в конце; ≤72 символов.
- **Тело** — только русский; полные предложения; пустая строка после заголовка.
- Запрещены vague-сообщения: `fix bug`, `update code`, `wip`, `misc`.
## Типы (semantic-release)
| type | Когда | Версия |
|------|--------|--------|
| `feat` | новая функциональность | minor |
| `fix` | исправление бага | patch |
| `perf` | ускорение без смены API | patch |
| `refactor` | реструктуризация без смены поведения | — |
| `docs` | только документация | — |
| `test` | тесты | — |
| `ci` | CI/CD (`.gitea/`, workflows) | — |
| `chore` | обслуживание, deps, `.cursor/` | — |
## Breaking changes
- Заголовок: `feat!` / `fix!` **или** в теле строка `BREAKING CHANGE:` (на английском ключевое слово) + описание impact **на русском**.
## Scope (EvoBGP)
Выбирать по доминирующему пути из staged diff:
| Префикс | scope |
|---------|-------|
| `internal/httpapi/` | `httpapi` |
| `internal/store/`, `internal/repository/`, `internal/db/` | `store` |
| `internal/jobs/` | `jobs` |
| `internal/pipeline/` | `pipeline` |
| `internal/birdfmt/` | `birdfmt` |
| `internal/birddeploy/` | `birddeploy` |
| `internal/bundle/`, `internal/signing/` | `bundle` |
| `cmd/` | `cmd` |
| `web/` | `web` |
| `docs/openapi.yaml`, `redocly.yaml` | `openapi` |
| `docs/` (остальное) | `docs` |
| `migrations/` | `db` |
| `.gitea/` | `ci` |
| `deploy/` | `deploy` |
| `.cursor/` | `chore` |
| прочее в корне | `chore` |
`type` определять по **содержимому diff**, не только по пути.
## Multi-change
- Несвязанные области → **отдельные коммиты** (auto-split по скиллу).
- Одна логическая фича через несколько scope (OpenAPI + httpapi + web) → **один** коммит, scope по главной области.
## Обязательный вывод агенту
Для каждого коммита:
**1. Готовое сообщение** (копировать в `git commit -m` / HEREDOC):
```
<type>(<scope>): <summary>
<тело RU>
```
**2. Пояснение (RU):** почему выбран type; риск/impact; был ли split.
## Примеры
```
feat(httpapi): add version endpoint and wire web footer
Добавлен GET /v1/version. В Web UI версия в footer берётся из API вместо захардкоженного значения.
```
```
fix(auth): correct token validation edge case
Исправлена ложная 401 при истёкшем refresh-токене с валидной сессией.
```
+241
View File
@@ -0,0 +1,241 @@
---
description: Инженерные правила EvoBGP — Go, API, migrations, общие стандарты
alwaysApply: true
---
# Engineering Guidelines — EvoBGP
Специализированные правила: **Web UI** → `.cursor/rules/web-shadcn.mdc`; **BIRD2 / BGP / IP** → `.cursor/rules/networking-bird.mdc`.
Карта репозитория: `AGENTS.md`, `docs/architecture.md`. HTTP-контракт: `docs/openapi.yaml`.
---
## Architecture
**ARCH-01** | MUST | Новая persistence-логика — метод `store.Backend` + реализации в `repository` и `store.Memory`; SQL не в `httpapi`.
*Rationale:* единая абстракция данных.
*Проверка:* grep SQL в `internal/httpapi` — отсутствие; review.
**ARCH-02** | MUST | HTTP-маршруты только в `internal/httpapi`; регистрация через `http.ServeMux` с паттернами `METHOD /v1/...`.
*Rationale:* один слой REST.
*Проверка:* маршруты только в `routes.go`, `routes_crud.go`.
**ARCH-03** | MUST | Долгие операции (refresh, apply, rollback) — `jobs.Registry`; ответ `202` + `job_id` где задано OpenAPI.
*Rationale:* не блокировать HTTP worker.
*Проверка:* OpenAPI + handlers.
**ARCH-04** | MUST | Кросс-процессные воркеры — HTTP к API или общая БД; не shared memory (кроме `evobgp-all`).
*Rationale:* `jobs.Registry` in-process only.
*Проверка:* `docs/architecture.md`; review.
**ARCH-05** | MUST | `birdfmt` не импортирует `httpapi` / `store`.
*Rationale:* направление зависимостей вниз.
*Проверка:* `go list -deps` / review imports.
**ARCH-06** | MUST | Точки входа `cmd/*` — тонкий `main`: config, wiring, signal/shutdown; без бизнес-логики.
*Rationale:* тестируемость `internal/`.
*Проверка:* review `main.go`.
**ARCH-07** | MUST | Инициализация store + jobs для API и воркеров — `httpapi.BootstrapWorkers`.
*Rationale:* единый wiring.
*Проверка:* `bootstrap.go`.
**ARCH-08** | MUST | Ingest+render префиксов — `internal/pipeline`; генерация BIRD-текста — `internal/birdfmt`.
*Rationale:* разделение data plane / control plane.
*Проверка:* review пакетов.
**ARCH-09** | NEVER | Прямой доступ handler'ов к `pgxpool` для CRUD; только `store.Backend`.
*Rationale:* абстракция бэкенда.
*Проверка:* review `httpapi`.
**ARCH-10** | MUST | Подпись бандлов — `internal/bundle` + `internal/signing`; не дублировать crypto в handlers.
*Rationale:* единая криптография артефактов.
*Проверка:* review.
---
## Code Style
**STYLE-01** | MUST | Go-код после `gofmt`; перед PR — `go vet ./...`.
*Rationale:* единый стиль.
*Проверка:* CI job `go`.
**STYLE-02** | MUST | Экспортируемые типы/функции публичных пакетов — godoc-комментарий.
*Rationale:* навигация по API пакетов.
*Проверка:* review.
**STYLE-03** | NEVER | `panic` в `internal/*` вне `init` и тестов.
*Rationale:* предсказуемые ошибки.
*Проверка:* grep `panic(`.
**STYLE-04** | MUST | Ошибки пакетов с префиксом (`birdfmt:`, `db:`, `httpapi:`) и `%w` при оборачивании.
*Rationale:* трассировка.
*Проверка:* review.
**STYLE-05** | MUST | HTTP-ошибки — `writeProblem` / `writeJSON` (`application/problem+json` для 4xx/5xx).
*Rationale:* RFC 9457, OpenAPI.
*Проверка:* `problem.go`.
**STYLE-06** | MUST | JSON полей HTTP DTO согласованы с `docs/openapi.yaml`.
*Rationale:* контракт API.
*Проверка:* OpenAPI diff + review.
---
## Dependency Management
**DEP-01** | MUST | Go-зависимости — через `go get` / `go.mod`; версия Go как в `go.mod` и CI (1.24).
*Проверка:* `go.mod`, `.gitea/workflows/ci.yaml`.
**DEP-02** | NEVER | Vendor-копирование без явного решения в репозитории.
*Проверка:* review.
**DEP-03** | MUST | Миграции схемы — пары `.up.sql`/`.down.sql` для **postgres** и **sqlite**, синхронная нумерация.
*Проверка:* `migrations/postgres/`, `migrations/sqlite/`.
**DEP-04** | MUST | Web UI-библиотеки — только экосистема shadcn-svelte/bits-ui (см. `web-shadcn.mdc`).
*Проверка:* `web/package.json` review.
---
## Error Handling
**ERR-01** | MUST | HTTP 5xx — без сырого `err.Error()` клиенту; detail через `writeProblem`.
*Проверка:* review handlers.
**ERR-02** | MUST | Публичные функции I/O — `(T, error)`; `errors.Is`/`errors.As` для sentinel.
*Проверка:* review.
**ERR-03** | MUST | `context.Context` — первый аргумент для I/O; таймауты на внешние HTTP (CDN, DoH, RIPEstat).
*Проверка:* `pipeline`, `bootstrap.go`.
---
## Testing
**TEST-01** | MUST | Перед PR: `go test ./... -race -count=1`.
*Проверка:* CI job `go`.
**TEST-02** | MUST | Табличные тесты — эталон для `birdfmt`, `pipeline`, `bundle`.
*Проверка:* `*_test.go`.
**TEST-03** | MUST | Новые BIRD-сценарии в `internal/birdfmt/testdata/scenarios/*/bird.conf` + `bird -p`.
*Проверка:* CI job `bird2`.
**TEST-04** | MUST | Изменения `web/` — локально `npm run check` и `npm run lint` (CI web пока не в scope).
*Проверка:* локальные команды.
**TEST-05** | MUST | Изменения OpenAPI — `npx @redocly/cli lint docs/openapi.yaml`.
*Проверка:* CI job `openapi`.
---
## Documentation
**DOC-01** | MUST | Пользовательская документация в `docs/` — на русском.
*Проверка:* review.
**DOC-02** | MUST | Изменение HTTP API: сначала `docs/openapi.yaml`, затем handlers/store, затем `docs/api.md` при необходимости.
*Проверка:* PR diff order.
**DOC-03** | MUST | Расхождение spec/код — явно в `docs/access.md` или description операции OpenAPI.
*Проверка:* review (напр. `X-Tenant-Id`).
**DOC-04** | NEVER | Дублировать длинные фрагменты OpenAPI в комментариях; ссылка на путь/operationId.
*Проверка:* review.
---
## Naming Conventions
**NAME-01** | MUST | Бинарники: `cmd/evobgp-<role>/main.go`.
*Проверка:* `cmd/`.
**NAME-02** | MUST | Env-переменные: префикс `EVOBGP_`.
*Проверка:* `internal/config`, `docs/access.md`.
**NAME-03** | MUST | Job kinds — константы в `internal/jobs/worker.go` (`module_refresh`, …).
*Проверка:* grep `Kind`.
**NAME-04** | MUST | ID сущностей — UUID-строки; tenant только из API-ключа (auth context), не из недокументированных заголовков.
*Проверка:* `auth.go`, `docs/access.md`.
---
## Performance
**PERF-01** | MUST | Списки API — пагинация `cursor` + `limit`; не unbounded выборки в handlers.
*Проверка:* OpenAPI + store methods.
**PERF-02** | MUST | CDN HTTP — переиспользуемый `http.Client` с таймаутом (45s в bootstrap).
*Проверка:* `bootstrap.go`.
**PERF-03** | MUST | Новые метрики — `internal/observability`, экспорт `/metrics`.
*Проверка:* review.
---
## Security
**SEC-01** | NEVER | Секреты, API-ключи, токены в Git.
*Проверка:* review; `docs/access.md`.
**SEC-02** | NEVER | `EVOBGP_DEV_INSECURE=1` в production.
*Проверка:* ops review.
**SEC-03** | MUST | Роль `node` — только node API; CRUD — `403` для `node`.
*Проверка:* `auth.go`, handlers.
**SEC-04** | MUST | apply/rollback/settings — operator (или выше по `roleLevel`).
*Проверка:* review handlers.
**SEC-05** | MUST | Бандл на ноде — `verify-bundle` перед `apply-bundle`.
*Проверка:* `docs/access.md`.
**SEC-06** | MUST | CORS — явный whitelist `EVOBGP_CORS_ORIGINS`.
*Проверка:* `cors.go`.
---
## Documentation Sync Rules
| Область | Источник |
|---------|----------|
| Go / net/http | https://go.dev/doc/ |
| pgx v5 | https://pkg.go.dev/github.com/jackc/pgx/v5 |
| OpenAPI / problem+json | `docs/openapi.yaml`, RFC 9457 |
| Svelte / Kit | https://svelte.dev/docs , https://kit.svelte.dev/docs |
| shadcn-svelte | https://shadcn-svelte.com/docs |
| BIRD 2 | https://bird.network.cz/?get_doc |
| Prometheus Go | https://pkg.go.dev/github.com/prometheus/client_golang |
**DOC-SYNC-01** | MUST | Новый API библиотеки — сверка версии в `go.mod`/`package.json` с официальной документацией.
**DOC-SYNC-02** | NEVER | Устаревшие примеры (Svelte 4 `export let`, deprecated pgx).
**DOC-SYNC-03** | MUST | Конфликт docs: **OpenAPI (HTTP)** → **код** → обзорные `docs/`; `.cursor/plans/` не контракт.
**DOC-SYNC-04** | MUST | Сомнения по Svelte — Svelte MCP / `npm run check`.
**DOC-SYNC-05** | MUST | BIRD — официальная документация BIRD2 + `networking-bird.mdc` + `go test ./internal/birdfmt/...`.
Приоритет при сомнениях — **официальные источники**, не блоги и не «память модели».
---
## Enforcement Strategy
**CI (Gitea):** OpenAPI lint; `go vet`, `go test -race`, build `cmd/*`; `bird -p` на scenarios; Docker bake на push.
**Локально перед PR:**
```powershell
go vet ./...
go test ./... -race -count=1
npx @redocly/cli lint docs/openapi.yaml
# web: cd web; npm run check; npm run lint
# birdfmt: go test ./internal/birdfmt/... -count=1
```
**Рекомендуется (частично внедрено):** CI job `web` (Gitea); `scripts/lint-httpapi.sh` в job `go`; `.golangci.yml` (локально); pre-commit gofmt/prettier.
**Только code review:** слои SQL; роли; idempotency; OpenAPI bodies; secrets в compose.
**Известные ограничения:** `X-Tenant-Id` в OpenAPI не реализован в handlers; `jobs.Registry` не shared между процессами API и отдельными воркерами без HTTP.
+219
View File
@@ -0,0 +1,219 @@
---
description: EvoBGP — BIRD2, BGP policy, IP/CIDR, маршрутизация, валидация конфигов
globs:
- internal/birdfmt/**
- internal/birddeploy/**
- internal/pipeline/**
- deploy/bird/**
- "**/testdata/scenarios/**"
alwaysApply: false
---
# Networking & Routing Rules
Общие правила: `.cursor/rules/engineering.mdc`. Пакет BIRD: `internal/birdfmt/doc.go`.
## Single source of truth
| Домен | SSOT |
|-------|------|
| Префиксы для анонса | Модули tenant → materialized snapshot → ревизия (`store`, `pipeline`, `CreateRenderRevision`) |
| Текст BIRD-фрагментов | `internal/birdfmt` — только генератор `evobgp_*.conf` |
| Каркас `bird.conf` | Operator skeleton + `include` (`StandardIncludeFragments`) |
| Параметры BIRD tenant | Global settings: `bird_router_id`, `bird_local_asn`, … (`docs/manual.md`) |
| BGP peers | `BGPPeer` + `ParsePeerNeighbor` |
**BIRD2 docs:** https://bird.network.cz/?get_doc
---
## A. BIRD2 Configuration Rules
**BIRD-01** | MUST | Имена фрагментов — константы `Fragment*` (`evobgp_prefixes_v4.conf`, …); include `bird.d/<name>`.
*Rationale:* совместимость deploy.
*Проверка:* `layout.go`; `go test ./internal/birdfmt/...`
**BIRD-02** | MUST | Порядок include: filters v4/v6 → BGP template → prefixes v4/v6 → peers (`StandardIncludeFragments`).
*Rationale:* фильтры до ссылок.
*Проверка:* scenario `standard_layout`; `bird -p`
**BIRD-03** | MUST | `bird.conf` (router id, device, direct, includes) отдельно от `bird.d/evobgp_*` (политика, префиксы).
*Rationale:* атомарная подмена фрагментов.
*Проверка:* review layout.
**BIRD-04** | MUST | Новая логика фильтрации — функция в `birdfmt` + unit-тест; NEVER дублировать filter-блок в два `.conf` вручную.
*Rationale:* DRY v4/v6.
*Проверка:* `filter_test.go`; review.
**BIRD-05** | MUST | Filter: префикс `evobgp_`, суффикс `_v4`/`_v6`; templates `bgp_template` / `bgp_template_v6`.
*Проверка:* `bgp.go`, `layout.go`.
**BIRD-06** | MUST | `router id` — валидный IPv4 dotted-quad (`RenderMainBirdConf`).
*Проверка:* `birdfmt` error; `bird -p`
**BIRD-07** | MUST | Communities — `RouteCommunityAttrs`; синтаксис `((a,b))` в `add()`, не `add(65000,1)`.
*Проверка:* `community_bird_test.go`
**BIRD-08** | SHOULD | Комментарии `#` в preamble; в фрагментах — revision/module id где уместно.
*Проверка:* review.
**BIRD-09** | NEVER | Ручное редактирование `evobgp_*.conf` на ноде без ревизии/бандла в control plane.
*Rationale:* drift.
*Проверка:* `evobgp-deploy` logs.
**BIRD-10** | MUST | Новый scenario: `testdata/scenarios/<name>/bird.conf` (+ `bird.d/`) проходит `bird -p`.
*Проверка:* CI `bird2`
**BIRD-11** | MUST | Только BIRD **2.x**, не 1.6.
*Проверка:* `bird --version`
**BIRD-12** | MUST | Apply: staging → `bird -p` → atomic swap → `birdc configure`; NEVER configure без parse check.
*Проверка:* `birdctl.go`; `birddeploy` review.
---
## B. Routing Policy Rules
**RTE-01** | MUST | BGP template: `import none` на каждый AFI, если задача явно не добавляет import policy.
*Rationale:* deny-by-default.
*Проверка:* `bgp.go`
**RTE-02** | MUST | Export filter завершается `reject;`.
*Проверка:* `filter.go`; `filter_export` scenario.
**RTE-03** | MUST | Анонс только префиксов materialized revision; NEVER static `route` вне pipeline/birdfmt без operator approval.
*Проверка:* store snapshot; review.
**RTE-04** | NEVER | Анонс `0.0.0.0/0`, `::/0`, RFC1918/ULA, loopback, link-local, multicast, reserved без documented exception.
*Проверка:* ingest validation; review.
**RTE-05** | NEVER | Префиксы чужих ASN без авторизации (IRR/RPKI — ops review).
*Проверка:* ops review.
**RTE-06** | MUST | Отдельные export filters IPv4 и IPv6.
*Проверка:* `RenderExportFilterIPv4/IPv6`
**RTE-07** | SHOULD | AS_PATH match — ASN из tenant modules; дедуп `filterUniqueASNs`.
*Проверка:* unit tests.
**RTE-08** | MUST | Peer import/export overrides — расширение `birdfmt`, не ad-hoc в operator `bird.conf`.
*Проверка:* review.
**RTE-09** | SHOULD | hold 90s / keepalive 30s — менять только с обоснованием в settings/docs.
*Проверка:* review.
**RTE-10** | MUST | После apply — post-check `birdc`/метрики (`mergeBirdPostApplyMeta`, `EVOBGP_BIRDC_SOCKET`).
*Проверка:* jobs meta; observability poller.
---
## C. IP Addressing Rules
**IP-01** | MUST | CIDR в Go — `netip.Prefix`; строка — `Masked().String()`.
*Проверка:* `pipeline/parse.go`
**IP-02** | MUST | IPv4/IPv6 — отдельные fragments/filters.
*Проверка:* `layout.go`
**IP-03** | MUST | BGP neighbor — `ParsePeerNeighbor`; `/32`/`/128` нормализуются в host addr.
*Проверка:* `peer_neighbor_test.go`
**IP-04** | NEVER | Hardcoded production CIDR вне testdata/fixtures.
*Проверка:* grep; review.
**IP-05** | MUST | Fixtures/docs — TEST-NET (RFC 5737, 3849): `203.0.113.0/24`, `198.51.100.0/24`, `2001:db8::/32`.
*Проверка:* scenarios.
**IP-06** | MUST | Domain→IP: host routes `/32`, `/128` via `ipToHostPrefix`; invalid отбрасывается.
*Проверка:* `collect_parallel.go`
**IP-07** | MUST | IP range entries — валидный CIDR на границе API/store.
*Проверка:* httpapi; review.
**IP-08** | SHOULD | Множество префиксов — без дубликатов; агрегация — policy decision.
*Проверка:* snapshot dedup.
**IP-09** | MUST | `router id`, `local as` — из tenant settings, не hardcode в callers `birdfmt`.
*Проверка:* `docs/manual.md`
**IP-10** | NEVER | Reserved blocks (`0.0.0.0/8`, `127.0.0.0/8`, `169.254.0.0/16`, `224.0.0.0/4`, IPv6 analogs) в materialized prefixes.
*Проверка:* review; future validator.
---
## D. Internet Compliance
**COMP-01** | SHOULD | Policy changes — сверка RFC 4271, 4760, 7454 (обзор).
*Проверка:* review.
**COMP-02** | MUST | No excessive deaggregation без need.
*Проверка:* review prefix list.
**COMP-03** | MUST | Standard community 0..65535; large — schema store.
*Проверка:* `community_bird_test.go`
**COMP-04** | NEVER | AS_PATH/MED manipulation без product spec.
*Проверка:* review output.
**COMP-05** | SHOULD | Revision/job meta: source, counts, samples (`revisionLogEntry`).
*Проверка:* jobs.
**COMP-06** | MUST | Node: `evobgp-node verify-bundle` перед apply.
*Проверка:* `docs/access.md`
**COMP-07** | SHOULD | `bird_local_asn` = `local as` в templates/peers.
*Проверка:* settings + birdfmt.
**COMP-08** | NEVER | Default route leak без named `default-originate` policy.
*Проверка:* export filter review.
---
## Validation & Observability
**VAL-01** | MUST | Перед деплоем: `bird -c <bird.conf> -p` (main + includes).
*Проверка:* CI `bird2`; `ParseCheck`
**VAL-02** | MUST | Preview ревизии read-only; apply — jobs/apply/bundle only.
*Проверка:* OpenAPI + httpapi.
**VAL-03** | MUST | Изменения birdfmt — `go test ./internal/birdfmt/...`.
*Проверка:* CI `go`
**VAL-04** | SHOULD | Dry-run apply где API поддерживает `dry_run`.
*Проверка:* routes.go.
**VAL-05** | MUST | `/metrics` Prometheus; `EVOBGP_BIRDC_SOCKET` — BGP session poller.
*Проверка:* `observability`
**VAL-06** | MUST | Логировать job_id, revision_id, speaker_id на apply/refresh/rollback.
*Проверка:* jobs API.
**VAL-07** | SHOULD | После apply — `birdc show protocols` / parsers в `birdfmt`.
*Проверка:* `protocols.go`
**VAL-08** | MUST | Мониторить drift: `last_applied_revision_id` vs published (`evobgp-deploy`).
*Проверка:* deploy worker.
---
## Documentation Sync
**DOC-SYNC-05** | MUST | BIRD — https://bird.network.cz/?get_doc
**DOC-SYNC-08** | MUST | BGP policy — RFC 4271, 4760, 7454 + BIRD docs + `birdfmt`
**DOC-SYNC-09** | MUST | CIDR — https://pkg.go.dev/net/netip ; примеры — RFC 5737, 3849
---
## Enforcement
```powershell
go test ./internal/birdfmt/... ./internal/pipeline/... -count=1
bird -c internal/birdfmt/testdata/scenarios/<name>/bird.conf -p
```
**PR checklist `birdfmt/**`, `pipeline/**`, `deploy/bird/**`:**
- [ ] filter + unit-test + `bird -p`
- [ ] нет дубли filter logic v4/v6 без общей Go-функции
- [ ] префиксы только store/revision path
- [ ] `import none` на templates сохранён
+109
View File
@@ -0,0 +1,109 @@
---
description: EvoBGP WebUI — shadcn-svelte, Svelte 5, слои ui/core|patterns|app
globs:
- web/**
alwaysApply: false
---
# Web UI — shadcn-svelte
**Источник правды:** https://shadcn-svelte.com/docs (не React shadcn/ui, не Legacy Docs).
Общие правила Go/API: `.cursor/rules/engineering.mdc`. Локальная карта: `web/README.md`.
## Слои UI
| Слой | Путь | Назначение |
|------|------|------------|
| Примитивы | `src/lib/ui/core/` | shadcn-svelte (только CLI `add`) |
| Паттерны | `src/lib/ui/patterns/` | FormField, AppDataTable, ConfirmDialog, EmptyState |
| App chrome | `src/lib/ui/app/` | Layout, PageHeader, `notify` |
| Legacy | `src/lib/components/ui/` | Re-export; **не добавлять новые файлы** |
Тема: `src/routes/layout.css`, `src/lib/ui/app/tokens.md`. CLI из `web/`: `npx shadcn-svelte@latest add <component> -y -o`.
---
## Правила
**WEB-01** | MUST | Перед новым UI — проверить https://shadcn-svelte.com/docs/components; использовать компонент, не HTML+CSS с нуля.
*Rationale:* Open Code + единый дизайн.
*Проверка:* review; нет голых `<button class=…>`.
**WEB-02** | MUST | Отсутствующий примитив — `npx shadcn-svelte@latest add <component> -y -o` → `src/lib/ui/core/`.
*Rationale:* Distribution через CLI и `components.json`.
*Проверка:* файлы только в `ui/core`.
**WEB-03** | NEVER | Альтернативные UI-kit'ы (Material, Vuetify, DaisyUI-only без shadcn-примитива).
*Проверка:* `package.json` review.
**WEB-04** | MUST | Комозиция по docs: все sub-компоненты (`DialogHeader`, `TableRow`, `Field`, …).
*Проверка:* сверка со страницей компонента в docs.
**WEB-05** | MUST | Формы — Formsnap + `sveltekit-superforms`; UI в `ui/patterns/form`, не ad-hoc валидация на странице.
*Проверка:* https://shadcn-svelte.com/docs/components/form
**WEB-06** | MUST | Таблицы — Data Table + `@tanstack/table-core`; на страницах — `AppDataTable` из patterns.
*Проверка:* https://shadcn-svelte.com/docs/components/data-table
**WEB-07** | MUST | Toast — Sonner через `notify` из `ui/app/toast.js`.
*Проверка:* https://shadcn-svelte.com/docs/components/sonner
**WEB-08** | MUST | Иконки — `@lucide/svelte` (`components.json` → `iconLibrary: lucide`).
*Проверка:* imports.
**WEB-09** | MUST | Цвета — CSS-переменные `layout.css` и токены `tokens.md`; не hex/rgb на страницах.
*Проверка:* grep `#[0-9a-f]{3,6}` в `routes/`.
**WEB-10** | SHOULD | Кастомизация — правка `ui/core` (Open Code), не `!important` поверх API.
*Проверка:* review.
**WEB-11** | MUST | `routes/**` — композиция `ui/core` + `ui/patterns` + `ui/app`; не копировать целые примитивы shadcn в route.
*Проверка:* review.
**WEB-12** | NEVER | Примеры React shadcn/ui или Svelte 4 Legacy без адаптации под https://shadcn-svelte.com/docs/migration/svelte-5
*Проверка:* `npm run check`.
**WEB-13** | MUST | Реактивность — Svelte 5 runes (`$state`, `$derived`, `$effect`); не `export let` для локального state страниц.
*Проверка:* `npm run check`; Svelte MCP.
**WEB-14** | SHOULD | Нетривиальный UI — прочитать страницу компонента (props, a11y).
*Проверка:* PR description.
**WEB-15** | MUST | Сомнения — https://shadcn-svelte.com/llms.txt , Svelte MCP, `npm run check`.
*Проверка:* локально.
**WEB-16** | MUST | Подтверждение удаления — `ConfirmDialog` из patterns, не `window.confirm`.
*Проверка:* review.
**WEB-17** | MUST | Пустые списки — `EmptyState` из patterns.
*Проверка:* review.
**WEB-18** | SHOULD | Повторяемая комбинация core (≥2 раза) — вынести в `ui/patterns/`.
*Проверка:* review.
---
## Documentation Sync (Web)
**DOC-SYNC-06** | MUST | UI — первично https://shadcn-svelte.com/docs; при конфликте с блогами/Stack Overflow побеждает официальная страница компонента.
**DOC-SYNC-07** | MUST | Перед `add` — сверить Installation/Theming с `web/components.json` и `src/routes/layout.css`.
Tailwind v4: https://shadcn-svelte.com/docs/migration/tailwind-v4
---
## Enforcement
```powershell
cd web
npm run check
npm run lint
```
**PR checklist `web/**`:**
- [ ] `ui/core` / `ui/patterns`, не дубли примитивов
- [ ] Новые примитивы через shadcn CLI
- [ ] Ссылка на docs компонента (если новый паттерн)
**CI:** job `web` рекомендован; пока обязательно локально.
+124
View File
@@ -0,0 +1,124 @@
---
name: commit-message
description: >-
ОБЯЗАТЕЛЬНО при commit, коммит, закоммить, commit message, conventional commit,
staged, semantic-release, «сгенерируй коммит», git commit: ПЕРВЫМ делом Shell —
scripts/commit/staged-context.ps1; затем Conventional Commit (заголовок EN, тело RU).
---
# Commit message (EvoBGP)
## Когда применять (сразу читать этот скилл)
Триггеры в промпте пользователя или в плане/AGENTS.md:
- `commit`, `коммит`, `закоммить`, `commit message`, `conventional commit`
- `git commit`, `staged`, `сообщение коммита`, `сгенерируй коммит`
- агент собирается выполнить `git commit` или предложить текст коммита
## Шаг 0 — ОБЯЗАТЕЛЬНО (до любого текста коммита)
**Первый вызов инструментов** в этой задаче — Shell из корня репозитория:
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
| Правило | Деталь |
|---------|--------|
| **MUST** | Запустить скрипт до генерации заголовка/тела коммита |
| **MUST NOT** | Строить сообщение только по `git diff --cached` / `git status` без скрипта |
| **MUST NOT** | Пропускать скрипт, даже если diff «и так понятен» |
| Exit `1` | Index пуст — сообщить пользователю, **не коммитить** |
| Exit `0` | Разобрать JSON stdout: `staged_count`, `groups[]` (`scope`, `files`, `stat`, `diff_excerpt`) |
Дополнительно: [.cursor/rules/conventional-commits.mdc](../../rules/conventional-commits.mdc).
## Слияние групп (одна фича)
Скрипт группирует **только по путям**. Перед split проверьте логическую связность:
**Объединить в один коммит**, если это одна задача:
- `docs/openapi.yaml` + `internal/httpapi/` (+ опционально `web/`) — один endpoint/контракт;
- `migrations/` + `internal/store/` / `repository/` — одна схема;
- правки теста рядом с кодом той же фичи.
При объединении: один scope (доминирующий пакет, часто `httpapi` или `openapi`), один type, одно тело RU.
**Auto-split** — если группы **не связаны** (например `web/` + `internal/birdfmt/` без общего смысла): **отдельный коммит на группу**, без вопроса пользователю.
## Порядок коммитов при split
1. `openapi`
2. `httpapi`, `store`, `jobs`
3. `pipeline`, `birdfmt`, `birddeploy`, `bundle`
4. `web`
5. `ci`, `deploy`, `db`
6. `docs`
7. `chore`, `cmd`
## Алгоритм auto-split (уровень файлов)
1. `git status` и `git diff --cached --stat` — зафиксировать полный список staged-файлов.
2. `git reset HEAD` — снять всё из index (working tree не трогать).
3. Для каждой (объединённой) группы по порядку выше:
- `git add -- <paths…>`
- Сгенерировать сообщение по правилу conventional-commits.
- Закоммитить (см. ниже).
4. После всех коммитов — `git status` для проверки.
**Ограничение:** частичный stage одного файла с разной семантикой — предупредить; split по hunk'ам не делать; предложить разнести правки по файлам.
## Создание коммита
Только если пользователь **явно** просил закоммитить. Иначе — только вывести готовые сообщения.
```powershell
git commit -m "$( @'
<type>(<scope>): <summary in English>
<Тело на русском.>
'@ )"
```
В PowerShell для многострочного тела используйте here-string как выше или `-m` для заголовка и `-m` для тела.
Перед коммитом: `git status`, `git diff --cached` для группы — убедиться, что stage соответствует сообщению.
## Генерация текста
По `groups[].diff_excerpt`, `stat`, `files`:
- **type** — по смыслу diff (`feat` / `fix` / …), не по умолчанию `chore`.
- **scope** — из JSON группы или доминирующий при merge.
- **summary** — конкретный, английский, императив.
- **body** — русский: что, зачем, edge cases, breaking impact.
## Вывод пользователю
Для **каждого** коммита:
### 1. Готовое сообщение
```
<type>(<scope>): <summary>
<тело>
```
### 2. Пояснение (RU)
- Почему выбран type/scope.
- Риски и impact.
- Split: сколько коммитов и почему.
## Не смешивать
Один commit message — одна primary intent. Не объединять несвязанный `fix` и `feat` в один заголовок.
## Ссылки
- Правило: `.cursor/rules/conventional-commits.mdc`
- Скрипт: `scripts/commit/staged-context.ps1`
- Просмотр групп: `powershell -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`
+29 -51
View File
@@ -1,75 +1,53 @@
# Gitea Actions
В репозитории включён workflow [workflows/ci.yaml](workflows/ci.yaml).
Workflow: [workflows/ci.yaml](workflows/ci.yaml).
Job **changes** вычисляет два флага:
| Job | PR | push в main |
|-----|-----|-------------|
| changes, openapi, web, go, bird2 | quality gates | quality gates |
| commitlint | да | — |
| **release** | — | semantic-release + docker push (один run) |
- **`openapi`** — в диффе есть `docs/openapi.yaml` или `redocly.yaml` → запускается **openapi** (Redocly lint).
- **`code`** — в диффе есть **хотя бы один файл вне «только документация»** → запускаются **go**, **bird2** и при push в main/master **docker-images**.
Подробнее: [docs/releasing.md](../docs/releasing.md).
Если изменены **только** файлы из списка ниже, **`code=false`**: Go-тесты, bird2 и сборка образов **не** запускаются; при необходимости всё ещё выполняется **openapi**, если трогали OpenAPI или `redocly.yaml`.
## CI (quality gates)
**Список путей «только документация» (без полной сборки):** всё под `docs/`, корневой `README.md`, `web/README.md`, `.gitea/README.md`, `redocly.yaml`.
Job **changes** вычисляет флаги по путям в diff. Изменение `.gitea/workflows/*` поднимает полный прогон.
Любой другой путь (в т.ч. `.gitea/workflows/ci.yaml`, `go.mod`, `web/package.json`, `deploy/**`, `**/*.go`) включает полный пайплайн.
На **pull request** — **commitlint** (Conventional Commits).
Требования к runner:
Runner: `ubuntu-latest`, **bird2** из apt, Docker для job **release**.
- Метка `ubuntu-latest` (или измените `runs-on` под свои labels).
- Job `bird2` ставит пакет **bird2** через `apt-get` на самом runner и вызывает `bird -c … -p` по файлам в checkout (без вложенного Docker: иначе bind-mount `/workspace/...` часто не виден демону Docker на хосте).
- Job **docker-images** требует **Docker** и (для push) доступ к Container Registry Gitea с runner’а.
## Release (job в ci.yaml)
## Сборка и публикация образов (`docker-images`)
После успешных quality gates на **push в main** job **release**:
Запускается только при **push** в ветки `main` или `master` (после успешного job `go`). Собираются образы из `deploy/docker/*` (все бинарники через `gobinary`, плюс `evobgp-web`, `evobgp-agent`, `evobgp-bird2`).
1. `npx semantic-release` — тег `vX.Y.Z` на **текущий commit** (без дополнительного commit в main).
2. Gitea Release + `CHANGELOG.md` как attachment (не в git).
3. `docker buildx bake default --push` с `VERSION=X.Y.Z` — в том же job.
### Секреты репозитория (для push в реестр)
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
Хост Container Registry в workflow **зашит**: **`git.shts.su`** (`docker/login-action` и теги образов).
### Секреты
В настройках репозитория Gitea: **Settings → Actions → Secrets** (или аналог):
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Fallback: **`gitea.token`**.
| Секрет | Назначение |
|--------|------------|
| `ACTIONS_PAT` | (Опционально) PAT с правом **записи пакетов**. Если не задан, для `docker login` используется **`gitea.token`** (нужны права у workflow на пакеты). |
Вход в реестр в workflow:
- **registry:** `git.shts.su`;
- **username:** `${{ gitea.actor }}`;
- **password:** `${{ secrets.ACTIONS_PAT || gitea.token }}`.
### Почему шаг «Log in to Gitea Registry» мог быть skipped
Раньше в `if:` стояло **`secrets.ACTIONS_PAT != ''`**. В **Gitea act_runner** такая проверка секрета в условии шага часто **всегда ложна**, из‑за этого логин **всегда skipped**. Сейчас условие только `docker_push == 'true'` (всегда при нормальном шаге **Prepare**), пароль: **`secrets.ACTIONS_PAT || gitea.token`**.
Если после правки `docker login` падает с 401/403 — выдайте job-токену или PAT право **записи в Container Registry** (в настройках Gitea / организации / репозитория, в зависимости от версии).
Если **`gitea.token`** в выражениях недоступен, в [ci.yaml](workflows/ci.yaml) замените пароль на `${{ secrets.ACTIONS_PAT || github.token }}`.
### Имена образов и теги
Формат:
### Теги образов
```text
git.shts.su/<owner_lowercase>/<имя>:<тег>
git.shts.su/<owner>/<имя>:latest
git.shts.su/<owner>/<имя>:v1.2.3
git.shts.su/<owner>/<имя>:1.2.3
git.shts.su/<owner>/<имя>:<short-sha>
git.shts.su/<owner>/<имя>:sha-<full-sha>
```
где `<имя>` — одно из: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all` (UI с прокси на `evobgp-all` для профиля Compose `microvps-full`), `evobgp-agent`, `evobgp-bird2`.
Имена образов: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all`, `evobgp-agent`, `evobgp-bird2`.
Теги: `latest`, короткий SHA (7 символов), `sha-<полный_commit_sha>`.
Пример pull:
Пример:
```bash
docker pull git.shts.su/myuser/evobgp-api:latest
docker pull git.shts.su/myuser/evobgp-api:1.2.3
```
Полная таблица образов, тегов, ссылок на страницы пакетов в Gitea и минимальный `docker run` для сервера без сборки — в [docs/quickstart.md](../docs/quickstart.md) (раздел «Готовые образы без сборки»).
Убедитесь, что в **Gitea** включены пакеты (Container Registry) и у токена есть права на создание/обновление образов. Другой хост реестра — правьте **`registry:`** и префикс **`IMG=`** в [ci.yaml](workflows/ci.yaml).
### Примечания
- Сборка с платформой `linux/amd64`. При необходимости добавьте `linux/arm64` и multi-arch в `docker buildx build`.
- Для PR job **docker-images** не выполняется (только push в main/master), чтобы не публиковать образы из чужих форков без явной политики.
См. [deploy/docker/README.md](../deploy/docker/README.md), [docs/quickstart.md](../docs/quickstart.md).
+111 -196
View File
@@ -81,7 +81,9 @@ jobs:
web/README.md) ;; # doc-only
web/*) web=true ;;
deploy/bird/*) bird_conf=true ;;
deploy/docker/bird/*) docker_bird=true; docker_go=true ;;
deploy/docker/gobinary/*) docker_go=true ;;
deploy/docker/docker-bake.hcl) docker_go=true; docker_web=true ;;
deploy/docker/evobgp-agent/*) docker_go=true ;;
deploy/docker/evobgp-web/*) docker_web=true ;;
deploy/docker/bird2/*) docker_bird=true ;;
@@ -119,6 +121,26 @@ jobs:
- name: Lint OpenAPI (Redocly)
run: npx --yes @redocly/cli@1 lint docs/openapi.yaml
# ---------------------------------------------------------------------------
web:
needs: [changes]
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- name: npm ci, check, lint
run: |
set -euxo pipefail
cd web
npm ci
npm run check
npm run lint
# ---------------------------------------------------------------------------
go:
needs: [changes]
@@ -128,9 +150,13 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version: "1.24"
cache: true
cache-dependency-path: go.sum
- name: Vet
run: go vet ./...
- name: Lint httpapi (ERR-01 / ARCH-01)
run: sh scripts/lint-httpapi.sh
- name: Test
run: go test ./... -race -count=1
- name: Build all commands
@@ -146,10 +172,18 @@ jobs:
# ---------------------------------------------------------------------------
bird2:
runs-on: ubuntu-latest
needs: [go]
needs: [changes, go]
if: >-
always() &&
needs.changes.result == 'success' &&
needs.go.result != 'failure' &&
(needs.changes.outputs.go == 'true' ||
needs.changes.outputs.bird_conf == 'true' ||
needs.changes.outputs.docker_bird == 'true' ||
needs.changes.outputs.docker_go == 'true')
steps:
- uses: actions/checkout@v4
- name: Install bird2
- name: Install bird2 (репозиторий Ubuntu runner, как в образе evobgp-bird2)
run: |
set -euxo pipefail
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=""; fi
@@ -174,224 +208,105 @@ jobs:
done
# ---------------------------------------------------------------------------
# Docker: Go-бинарники (api, all, scheduler, ingest, render, deploy, node, agent).
# Запускается если изменился Go-код или Go-Dockerfile.
# Если Go-код менялся — требуем успех go-тестов; если только Dockerfile — go skipped, ОК.
commitlint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- name: Lint commit messages
run: |
set -euxo pipefail
npm ci
npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"
# ---------------------------------------------------------------------------
docker-go:
needs: [changes, go]
# Один push в main: semantic-release (тег на текущий commit, без доп. commit) + docker push.
# ---------------------------------------------------------------------------
release:
needs: [changes, openapi, web, go, bird2]
if: >-
always() &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
needs.changes.result == 'success' &&
needs.go.result != 'failure' &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.go == 'true' || needs.changes.outputs.docker_go == 'true')
(needs.openapi.result == 'success' || needs.openapi.result == 'skipped') &&
(needs.web.result == 'success' || needs.web.result == 'skipped') &&
(needs.go.result == 'success' || needs.go.result == 'skipped') &&
(needs.bird2.result == 'success' || needs.bird2.result == 'skipped')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- image: evobgp-api
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-api
birdc: "1"
evobgp_upstream: ""
- image: evobgp-all
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-all
birdc: "1"
evobgp_upstream: ""
- image: evobgp-scheduler
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-scheduler
birdc: "0"
evobgp_upstream: ""
- image: evobgp-ingest
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-ingest
birdc: "0"
evobgp_upstream: ""
- image: evobgp-render
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-render
birdc: "0"
evobgp_upstream: ""
- image: evobgp-deploy
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-deploy
birdc: "0"
evobgp_upstream: ""
- image: evobgp-node
dockerfile: deploy/docker/gobinary/Dockerfile
bin: evobgp-node
birdc: "0"
evobgp_upstream: ""
- image: evobgp-agent
dockerfile: deploy/docker/evobgp-agent/Dockerfile
evobgp_upstream: ""
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
persist-credentials: true
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- name: Install release tooling
run: npm ci
- name: Semantic release
run: npx semantic-release
env:
GITEA_URL: https://git.shts.su
GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Detect new release
id: rel
run: |
set -euo pipefail
if [ -f .release-version ]; then
echo "version=$(tr -d '[:space:]' < .release-version)" >> "$GITHUB_OUTPUT"
echo "released=true" >> "$GITHUB_OUTPUT"
else
echo "released=false" >> "$GITHUB_OUTPUT"
echo "No releasable commits — skipping image publish"
fi
- name: Set up Docker Buildx
if: steps.rel.outputs.released == 'true'
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
if: steps.rel.outputs.released == 'true'
id: meta
run: |
set -euo pipefail
echo "version=${{ steps.rel.outputs.version }}" >> "$GITHUB_OUTPUT"
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
if: steps.rel.outputs.released == 'true'
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push ${{ matrix.image }}
- name: Build and push images (bake)
if: steps.rel.outputs.released == 'true'
env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
IMAGE_TAG: latest
VERSION: ${{ steps.meta.outputs.version }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
DF: ${{ matrix.dockerfile }}
IMAGE: ${{ matrix.image }}
BIN: ${{ matrix.bin }}
BIRDC: ${{ matrix.birdc }}
EVOBGP_UPSTREAM: ${{ matrix.evobgp_upstream }}
SHA_FULL: ${{ github.sha }}
BUILD_TIME: ${{ steps.meta.outputs.build_time }}
CACHE_REF_GO: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache
CACHE_REF_WEB: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
run: |
set -euxo pipefail
WS="${{ github.workspace }}"
cd "$WS"
cd "${{ github.workspace }}/deploy/docker"
sh write-bake-override.sh
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
-f docker-bake.hcl -f docker-bake.override.hcl default --push
IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
BUILD_ARGS=()
if [ -n "${BIN:-}" ]; then
BUILD_ARGS+=(--build-arg "BIN=${BIN}")
BUILD_ARGS+=(--build-arg "INSTALL_BIRDC=${BIRDC:-0}")
fi
if [ -n "${EVOBGP_UPSTREAM:-}" ]; then
BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}")
fi
docker buildx build \
--platform linux/amd64 \
--file "$DF" \
"${BUILD_ARGS[@]}" \
--tag "${IMG}:latest" \
--tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:sha-${{ github.sha }}" \
--push \
"$WS"
# ---------------------------------------------------------------------------
# Docker: Web-фронтенд (evobgp-web, evobgp-web-all).
# Не зависит от Go-тестов — SvelteKit собирается отдельно.
# ---------------------------------------------------------------------------
docker-web:
needs: [changes]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.docker_web == 'true')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- image: evobgp-web
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: ""
- image: evobgp-web-all
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: evobgp-all
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push ${{ matrix.image }}
env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
DF: ${{ matrix.dockerfile }}
IMAGE: ${{ matrix.image }}
EVOBGP_UPSTREAM: ${{ matrix.evobgp_upstream }}
run: |
set -euxo pipefail
WS="${{ github.workspace }}"
cd "$WS"
IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
BUILD_ARGS=()
if [ -n "${EVOBGP_UPSTREAM:-}" ]; then
BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}")
fi
docker buildx build \
--platform linux/amd64 \
--file "$DF" \
"${BUILD_ARGS[@]}" \
--tag "${IMG}:latest" \
--tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:sha-${{ github.sha }}" \
--push \
"$WS"
# ---------------------------------------------------------------------------
# Docker: BIRD2 (evobgp-bird2).
# Собирается только при изменении deploy/bird/ или Dockerfile bird2.
# ---------------------------------------------------------------------------
docker-bird:
needs: [changes]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.bird_conf == 'true' || needs.changes.outputs.docker_bird == 'true')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push evobgp-bird2
env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
run: |
set -euxo pipefail
WS="${{ github.workspace }}"
IMG="git.shts.su/${OWNER_LC}/evobgp-bird2"
docker buildx build \
--platform linux/amd64 \
--file deploy/docker/bird2/Dockerfile \
--tag "${IMG}:latest" \
--tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:sha-${{ github.sha }}" \
--push \
"$WS"
+18
View File
@@ -0,0 +1,18 @@
# Root npm (semantic-release, commitlint) — npm ci in CI, never commit deps
node_modules/
# Generated by deploy/docker/write-bake-override.sh (CI/local bake)
deploy/docker/docker-bake.override.hcl
# CI-only markers (not committed)
.release-version
CHANGELOG.md
# OS
.DS_Store
Thumbs.db
# Env / secrets (local only)
.env
.env.*
!.env.example
+17
View File
@@ -0,0 +1,17 @@
run:
timeout: 5m
linters:
enable:
- gofmt
- govet
- errcheck
issues:
exclude-use-default: false
max-issues-per-linter: 50
max-same-issues: 20
linters-settings:
gofmt:
simplify: true
+48
View File
@@ -0,0 +1,48 @@
{
"branches": ["main", "master"],
"tagFormat": "v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "breaking": true, "release": "major" }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/exec",
{
"successCmd": "echo ${nextRelease.version} > .release-version"
}
],
[
"@markwylde/semantic-release-gitea",
{
"giteaUrl": "https://git.shts.su",
"assets": [
{
"path": "CHANGELOG.md",
"label": "Changelog"
}
]
}
]
]
}
+59
View File
@@ -0,0 +1,59 @@
# Руководство для ИИ-агентов (экономия контекста)
Краткие ориентиры по репозиторию **EvoBGP**, чтобы не тратить токены на полное сканирование дерева и повторное чтение одних и тех же файлов.
## С чего начать (минимум чтения)
0. **Инженерные правила** — при изменении кода следовать [.cursor/rules/engineering.mdc](.cursor/rules/engineering.mdc); для `web/` — [.cursor/rules/web-shadcn.mdc](.cursor/rules/web-shadcn.mdc); для `birdfmt` / `pipeline` / BIRD — [.cursor/rules/networking-bird.mdc](.cursor/rules/networking-bird.mdc).
1. **[docs/README.md](docs/README.md)** — оглавление и роли читателя.
2. **[docs/architecture.md](docs/architecture.md)** — компоненты `cmd/`, карта `internal/`, потоки данных (одного этого файла обычно достаточно для ориентации).
3. Задача-специфично: [docs/api.md](docs/api.md), [docs/access.md](docs/access.md), [web/README.md](web/README.md) — только если меняете API, доступ или фронт.
Источник правды по HTTP-контракту: **[docs/openapi.yaml](docs/openapi.yaml)**. Не дублируйте длинные фрагменты спецификации в ответах — ссылайтесь на путь и тег/операцию.
## Карта кода (куда смотреть)
| Область | Где искать |
|---------|------------|
| REST, auth, CORS | `internal/httpapi/` |
| Бизнес-слой и абстракция хранилища | `internal/store/` |
| PostgreSQL | `internal/repository/`, `internal/db/`, `migrations/` |
| Фоновые задачи | `internal/jobs/` |
| Цепочка refresh модуля (ingest+render, BIRD preview) | `internal/pipeline/` |
| Конфиг BIRD, `birdc` | `internal/birdfmt/`, `internal/birddeploy/` |
| Бандлы и подписи | `internal/bundle/`, `internal/signing/` |
| Точки входа процессов | `cmd/*/` |
| Веб (SvelteKit) | `web/` |
| Compose, деплой | `deploy/compose/` |
Точки входа бинарников и их роли — в таблице в начале [docs/architecture.md](docs/architecture.md).
## Как не раздувать контекст
- **Сначала узкий поиск:** `grep`/поиск по символу или короткий семантический запрос по одной папке (`internal/httpapi/`, `internal/pipeline/`, …), а не чтение всех `.go` подряд.
- **Читайте файлы целиком только при необходимости:** большие файлы — с `offset`/`limit` или по найденным строкам.
- **Не подтягивайте в контекст:** `web/node_modules/`, сгенерированные артефакты сборки, бинарники, полный `openapi.html`, если достаточно `openapi.yaml`.
- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи.
- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины.
## Коммиты (Conventional Commits)
Если пользователь просит **коммит**, **commit message**, **закоммить**, **git commit** или это следует из плана — **сразу**:
1. Shell: `powershell -NoProfile -File scripts/commit/staged-context.ps1` (первый вызов, до текста коммита).
2. Скилл [.cursor/skills/commit-message/SKILL.md](.cursor/skills/commit-message/SKILL.md) и правило [.cursor/rules/conventional-commits.mdc](.cursor/rules/conventional-commits.mdc).
Без вывода скрипта (exit 0) **не** придумывать сообщение коммита. Заголовок — EN, тело — RU; несвязанные области — auto-split (скилл).
## Команды и среда
- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`.
- Быстрый старт и переменные: [docs/quickstart.md](docs/quickstart.md), [README.md](README.md).
## Язык документации проекта
Пользовательская документация в `docs/` — преимущественно на русском. Комментарии и имена в коде — в существующем стиле репозитория.
## Svelte / фронтенд
При правках `web/**/*.svelte` или Svelte-модулей следуйте навыкам/инструментам проекта (официальный Svelte MCP и скиллы Cursor, если подключены).
+150
View File
@@ -0,0 +1,150 @@
## 1.0.0 (2026-05-19)
* feat: add @tanstack/table-core dependency and integrate module name mapping in operations page. Upda ([b140dcb](http://192.168.100.56:3000/denozord/EvoBGP/commit/b140dcb))
* feat: add @types/node and undici-types to devDependencies in package.json and package-lock.json for ([d92a06c](http://192.168.100.56:3000/denozord/EvoBGP/commit/d92a06c))
* feat: add aggregated router-lists catalog endpoint and update module listing filters ([c811c43](http://192.168.100.56:3000/denozord/EvoBGP/commit/c811c43))
* feat: add auto-updater service for Docker stack with configurable settings ([ae9fbe3](http://192.168.100.56:3000/denozord/EvoBGP/commit/ae9fbe3))
* feat: add bird configuration preview functionality with tabbed interface for fragments and prefixes. ([1969a00](http://192.168.100.56:3000/denozord/EvoBGP/commit/1969a00))
* feat: add BIRD status endpoint and integrate birdc checks into job processing. Enhance UI to display ([94a4c6a](http://192.168.100.56:3000/denozord/EvoBGP/commit/94a4c6a))
* feat: add CSV import and export functionality for module entries. Implement endpoints for exporting ([bae2d68](http://192.168.100.56:3000/denozord/EvoBGP/commit/bae2d68))
* feat: add debug logging and optimize database queries in Postgres repository ([6d0e214](http://192.168.100.56:3000/denozord/EvoBGP/commit/6d0e214))
* feat: add diagnostic log endpoint for revisions and implement download functionality ([2260ecf](http://192.168.100.56:3000/denozord/EvoBGP/commit/2260ecf))
* feat: add enabled state to BGP peer types and UI. Implement peer enabling toggle in the network page ([80965ad](http://192.168.100.56:3000/denozord/EvoBGP/commit/80965ad))
* feat: add last refreshed timestamp to modules and CDN sources ([1e13ef9](http://192.168.100.56:3000/denozord/EvoBGP/commit/1e13ef9))
* feat: add live BGP protocol state tracking to peer listing ([8a50ba4](http://192.168.100.56:3000/denozord/EvoBGP/commit/8a50ba4))
* feat: add optional name field to BGP peer creation and update UI ([1a61ae2](http://192.168.100.56:3000/denozord/EvoBGP/commit/1a61ae2))
* feat: add optional name field to DoH profile schema and types ([6bee5ad](http://192.168.100.56:3000/denozord/EvoBGP/commit/6bee5ad))
* feat: add smart aggregation for IPv4 CIDR prefixes in refresh pipeline. Implement functionality to a ([0428b83](http://192.168.100.56:3000/denozord/EvoBGP/commit/0428b83))
* feat: add strict bind configuration to BGP templates. Enhance BGP template rendering by including 's ([07f2ce5](http://192.168.100.56:3000/denozord/EvoBGP/commit/07f2ce5))
* feat: add tenant refresh functionality to HTTP API and job processing ([7a2b015](http://192.168.100.56:3000/denozord/EvoBGP/commit/7a2b015))
* feat: auto-refresh non-IP module updates and add full project manual ([1e77e1a](http://192.168.100.56:3000/denozord/EvoBGP/commit/1e77e1a))
* feat: enhance API client with pagination and new data fetching functions. Implement `apiPageAll` for ([9f0b128](http://192.168.100.56:3000/denozord/EvoBGP/commit/9f0b128))
* feat: enhance BGP template and peer configuration handling. Introduce policies_json parameter for lo ([be8e7b5](http://192.168.100.56:3000/denozord/EvoBGP/commit/be8e7b5))
* feat: enhance CDN source management with last refreshed timestamp ([4776434](http://192.168.100.56:3000/denozord/EvoBGP/commit/4776434))
* feat: enhance community attribute handling in BIRD format. Add validation for community route bodies ([b541794](http://192.168.100.56:3000/denozord/EvoBGP/commit/b541794))
* feat: enhance community label handling and improve UI localization across operations pages. Introduc ([53fd527](http://192.168.100.56:3000/denozord/EvoBGP/commit/53fd527))
* feat: enhance deployment configuration for secure Web UI with Traefik integration ([4acd685](http://192.168.100.56:3000/denozord/EvoBGP/commit/4acd685))
* feat: enhance DoH profile management and resolver policy in modules ([34ecc5c](http://192.168.100.56:3000/denozord/EvoBGP/commit/34ecc5c))
* feat: enhance EvoBGP with new command-line options for the evobgp-agent, including a watch command f ([6a55f72](http://192.168.100.56:3000/denozord/EvoBGP/commit/6a55f72))
* feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and ([bf52b21](http://192.168.100.56:3000/denozord/EvoBGP/commit/bf52b21))
* feat: enhance job detail handling in UI with caching and loading states. Introduce SvelteMap for job ([05bc5c4](http://192.168.100.56:3000/denozord/EvoBGP/commit/05bc5c4))
* feat: enhance job detail UI with improved overflow handling and layout adjustments. Update card comp ([67ce135](http://192.168.100.56:3000/denozord/EvoBGP/commit/67ce135))
* feat: enhance job filtering and display functionality in operations page. Introduce job search and f ([e1c4cbc](http://192.168.100.56:3000/denozord/EvoBGP/commit/e1c4cbc))
* feat: enhance job processing with detailed logging and UI improvements. Introduce revision log entri ([c48cc4e](http://192.168.100.56:3000/denozord/EvoBGP/commit/c48cc4e))
* feat: enhance monitoring page with detailed status tracking and error handling ([458a885](http://192.168.100.56:3000/denozord/EvoBGP/commit/458a885))
* feat: enhance observability with prefix aggregation metrics ([07b97bd](http://192.168.100.56:3000/denozord/EvoBGP/commit/07b97bd))
* feat: enhance peer neighbor handling by introducing NormalizePeerNeighborString function. Update Cre ([80f3a8d](http://192.168.100.56:3000/denozord/EvoBGP/commit/80f3a8d))
* feat: enhance RefreshModule to aggregate prefixes from all enabled tenant modules. Update documentat ([431c6a9](http://192.168.100.56:3000/denozord/EvoBGP/commit/431c6a9))
* feat: enhance RefreshModule to support legacy ASN resolution and improve prefix handling. Introduce ([e1aaa18](http://192.168.100.56:3000/denozord/EvoBGP/commit/e1aaa18))
* feat: enhance settings page layout and section management ([d91667f](http://192.168.100.56:3000/denozord/EvoBGP/commit/d91667f))
* feat: enhance UI components with improved styling and layout adjustments. Update button sizes, text ([6a72842](http://192.168.100.56:3000/denozord/EvoBGP/commit/6a72842))
* feat: enhance worker functionality by adding job queuing for deploy_apply. Introduce a Registry fiel ([42d956d](http://192.168.100.56:3000/denozord/EvoBGP/commit/42d956d))
* feat: implement AS holder name resolution and enhance ASEntry structure. Add ASHolderName function t ([c958d5a](http://192.168.100.56:3000/denozord/EvoBGP/commit/c958d5a))
* feat: implement bulk selection and deletion for modules and entries. Add functionality for selecting ([e816d17](http://192.168.100.56:3000/denozord/EvoBGP/commit/e816d17))
* feat: implement CDN source preview functionality and enhance data handling. Add a new endpoint for p ([e20c9f3](http://192.168.100.56:3000/denozord/EvoBGP/commit/e20c9f3))
* feat: implement countAll function for API data retrieval ([40c8a8d](http://192.168.100.56:3000/denozord/EvoBGP/commit/40c8a8d))
* feat: implement domain resolution via DoH in refresh pipeline. Enhance the collectModulePrefixRows f ([678bd27](http://192.168.100.56:3000/denozord/EvoBGP/commit/678bd27))
* feat: implement effective peer enabling logic during peer creation. Update CreatePeer methods in Pos ([3e85431](http://192.168.100.56:3000/denozord/EvoBGP/commit/3e85431))
* feat: implement module refresh job management with concurrency control. Add CountOtherActiveModuleRe ([149fb0a](http://192.168.100.56:3000/denozord/EvoBGP/commit/149fb0a))
* feat: implement peer reconciliation job for BGP peers ([3b17228](http://192.168.100.56:3000/denozord/EvoBGP/commit/3b17228))
* feat: implement revision retention settings and pruning logic ([4f50350](http://192.168.100.56:3000/denozord/EvoBGP/commit/4f50350))
* feat: implement theme selection and automatic theme application based on user preference. Update App ([b70a643](http://192.168.100.56:3000/denozord/EvoBGP/commit/b70a643))
* feat: implement versioning and release management in the project ([bb334b1](http://192.168.100.56:3000/denozord/EvoBGP/commit/bb334b1))
* feat: improve BGP protocol summary handling by introducing isBGPProtocolSummaryRow function. Update ([399622c](http://192.168.100.56:3000/denozord/EvoBGP/commit/399622c))
* feat: improve job detail UI with enhanced spacing and layout adjustments. Update table cell classes ([97908b5](http://192.168.100.56:3000/denozord/EvoBGP/commit/97908b5))
* feat: improve peer speaker assignment UX in network settings ([f877ec8](http://192.168.100.56:3000/denozord/EvoBGP/commit/f877ec8))
* feat: improve UI components with enhanced overflow handling and layout adjustments. Update card and ([0ca5542](http://192.168.100.56:3000/denozord/EvoBGP/commit/0ca5542))
* feat: improve UI layout and responsiveness in module and operations pages. Refactor component struct ([233f29b](http://192.168.100.56:3000/denozord/EvoBGP/commit/233f29b))
* feat: redesign settings page with structured controls ([2da6d49](http://192.168.100.56:3000/denozord/EvoBGP/commit/2da6d49))
* feat: refactor materialization logic to group static routes by community in the refresh pipeline. In ([1c5a9c9](http://192.168.100.56:3000/denozord/EvoBGP/commit/1c5a9c9))
* feat: refactor operations page to enhance UI and component structure. Remove unused card components ([88226f6](http://192.168.100.56:3000/denozord/EvoBGP/commit/88226f6))
* feat: refine BGP template handling and documentation updates. Remove local IP requirements in BGP te ([6266a39](http://192.168.100.56:3000/denozord/EvoBGP/commit/6266a39))
* feat: refine job detail UI with improved layout and scrolling capabilities. Enhance spacing and over ([57c09ac](http://192.168.100.56:3000/denozord/EvoBGP/commit/57c09ac))
* feat: update BGP community structure and related documentation. Refactor BGP community handling by r ([a2cbfac](http://192.168.100.56:3000/denozord/EvoBGP/commit/a2cbfac))
* feat: update BGP template configuration and OpenAPI documentation. Change import settings from 'all' ([133ea93](http://192.168.100.56:3000/denozord/EvoBGP/commit/133ea93))
* feat: update BIRD installation process to build from source. Modify CI workflow to ensure BIRD 2.14 ([e2002ba](http://192.168.100.56:3000/denozord/EvoBGP/commit/e2002ba))
* feat: update CI configuration and Dockerfiles for improved build efficiency ([80229b3](http://192.168.100.56:3000/denozord/EvoBGP/commit/80229b3))
* feat: update collectModulePrefixRows to support prior snapshots and enhance CDN/domain prefix collec ([ee364c8](http://192.168.100.56:3000/denozord/EvoBGP/commit/ee364c8))
* feat: update Go module dependencies and enhance DNS resolution in refresh pipeline. Upgrade Go versi ([655d750](http://192.168.100.56:3000/denozord/EvoBGP/commit/655d750))
* feat: update OpenAPI documentation and internal logic for BIRD settings. Enhance the description of ([62bf987](http://192.168.100.56:3000/denozord/EvoBGP/commit/62bf987))
* feat: update RevisionDiff type and enhance OperationsDiffTab component for better diff handling. Int ([4aaaa30](http://192.168.100.56:3000/denozord/EvoBGP/commit/4aaaa30))
* feat: update Traefik deployment configuration for Let's Encrypt and logging ([39ba976](http://192.168.100.56:3000/denozord/EvoBGP/commit/39ba976))
* docs: add router-lists-ui integration documentation to README ([b7a8aab](http://192.168.100.56:3000/denozord/EvoBGP/commit/b7a8aab))
* docs: add Web UI section to EvoBGP architecture plan, detailing Svelte and shadcn-svelte integration ([272542b](http://192.168.100.56:3000/denozord/EvoBGP/commit/272542b))
* docs: enhance EvoBGP architecture plan with new risk mitigation strategies, testing matrix for BIRD2 ([3992d01](http://192.168.100.56:3000/denozord/EvoBGP/commit/3992d01))
* docs: enhance README and CI workflow documentation for Docker image building and publishing process, ([9eb3e33](http://192.168.100.56:3000/denozord/EvoBGP/commit/9eb3e33))
* docs: enhance README and CI workflow to clarify Docker image build conditions and Gitea Container Re ([c6687b2](http://192.168.100.56:3000/denozord/EvoBGP/commit/c6687b2))
* docs: openapi.html со встроенным Redoc; инструкция для Gitea и скрипт сборки ([a754e1d](http://192.168.100.56:3000/denozord/EvoBGP/commit/a754e1d))
* docs: update AGENTS and README to include guidelines for Conventional Commits ([4c23232](http://192.168.100.56:3000/denozord/EvoBGP/commit/4c23232))
* docs: update AGENTS and README with engineering rules and guidelines ([aff27e8](http://192.168.100.56:3000/denozord/EvoBGP/commit/aff27e8))
* docs: update architecture and API documentation to clarify job processing, in-memory registry usage, ([acc0608](http://192.168.100.56:3000/denozord/EvoBGP/commit/acc0608))
* docs: update EvoBGP architecture plan with detailed REST API section, including general agreements, ([1a86d6c](http://192.168.100.56:3000/denozord/EvoBGP/commit/1a86d6c))
* docs: update EvoBGP architecture plan with new repository structure, refined sections, and detailed ([4480d64](http://192.168.100.56:3000/denozord/EvoBGP/commit/4480d64))
* docs: update README and CI workflow to clarify Docker workspace mounting for bird2 job ([6be46db](http://192.168.100.56:3000/denozord/EvoBGP/commit/6be46db))
* docs: update README and CI workflow to clarify Gitea Registry login process and improve Docker image ([f1b33b9](http://192.168.100.56:3000/denozord/EvoBGP/commit/f1b33b9))
* docs: update README and Docker Compose configurations to clarify usage of pre-built images from the ([7fac79c](http://192.168.100.56:3000/denozord/EvoBGP/commit/7fac79c))
* docs: update README to include information about the EvoBGP web interface, linking to quickstart and ([5d21f01](http://192.168.100.56:3000/denozord/EvoBGP/commit/5d21f01))
* docs: статическая Redoc-страница OpenAPI (для просмотра в Gitea без встроенного рендера) ([5c9d2e2](http://192.168.100.56:3000/denozord/EvoBGP/commit/5c9d2e2))
* docs: убрать типографское тире и невидимый Unicode из текстов API ([fbe9cd3](http://192.168.100.56:3000/denozord/EvoBGP/commit/fbe9cd3))
* refactor: adjust BootstrapWorkers function call in server.go to change the order of returned values, ([032fa99](http://192.168.100.56:3000/denozord/EvoBGP/commit/032fa99))
* refactor: consolidate module interval display logic and improve edit form handling ([1f85654](http://192.168.100.56:3000/denozord/EvoBGP/commit/1f85654))
* refactor: enhance bird metrics polling with context support ([f6b94a4](http://192.168.100.56:3000/denozord/EvoBGP/commit/f6b94a4))
* refactor: enhance CI workflow for granular change detection by module and improve idempotency key ge ([54eaad3](http://192.168.100.56:3000/denozord/EvoBGP/commit/54eaad3))
* refactor: enhance job status checks in worker tests ([6107c3e](http://192.168.100.56:3000/denozord/EvoBGP/commit/6107c3e))
* refactor: enhance JSON struct tags for various data models in backend and memory store. Update field ([4a860a0](http://192.168.100.56:3000/denozord/EvoBGP/commit/4a860a0))
* refactor: enhance module display and input handling in module pages ([25374a6](http://192.168.100.56:3000/denozord/EvoBGP/commit/25374a6))
* refactor: enhance module interval label logic and add reset buttons in edit forms ([b99fc17](http://192.168.100.56:3000/denozord/EvoBGP/commit/b99fc17))
* refactor: improve code consistency and formatting across components ([c263fd5](http://192.168.100.56:3000/denozord/EvoBGP/commit/c263fd5))
* refactor: improve handling of deployable fragments in ApplyRevision and buildPreviewFragments functi ([6932271](http://192.168.100.56:3000/denozord/EvoBGP/commit/6932271))
* refactor: improve module save functionality and nullable value handling ([05ec176](http://192.168.100.56:3000/denozord/EvoBGP/commit/05ec176))
* refactor: improve PATCH handling for module updates with nullable field support ([e700f90](http://192.168.100.56:3000/denozord/EvoBGP/commit/e700f90))
* refactor: optimize job listing and counting logic for concurrency safety. Separate tenant filtering ([d83efe2](http://192.168.100.56:3000/denozord/EvoBGP/commit/d83efe2))
* refactor: phase 2 structural alignment — reports, importer, nodecli, pagination ([250cdea](http://192.168.100.56:3000/denozord/EvoBGP/commit/250cdea))
* refactor: remove strict bind configuration from BGP templates and update related logic. Eliminate 's ([482eeb1](http://192.168.100.56:3000/denozord/EvoBGP/commit/482eeb1))
* refactor: streamline Docker build process in CI configuration and documentation ([6025f38](http://192.168.100.56:3000/denozord/EvoBGP/commit/6025f38))
* refactor: streamline edit form handling for community and DoH profile selections ([1765ab8](http://192.168.100.56:3000/denozord/EvoBGP/commit/1765ab8))
* refactor: streamline module refresh process and enhance revision handling ([10e2415](http://192.168.100.56:3000/denozord/EvoBGP/commit/10e2415))
* refactor: switch BIRD installation from source to Ubuntu package. Update CI workflow and Dockerfiles ([0b3ceaa](http://192.168.100.56:3000/denozord/EvoBGP/commit/0b3ceaa))
* refactor: update AS entry handling in API and database to support ASN-only entries. Remove prefix fi ([1db6b5b](http://192.168.100.56:3000/denozord/EvoBGP/commit/1db6b5b))
* refactor: update Docker build context and paths in CI configuration and documentation ([7d52a4a](http://192.168.100.56:3000/denozord/EvoBGP/commit/7d52a4a))
* refactor: update Docker build variable syntax in CI configuration and documentation ([27e01d0](http://192.168.100.56:3000/denozord/EvoBGP/commit/27e01d0))
* refactor: update job kind display logic in schedule page ([dc1f4a5](http://192.168.100.56:3000/denozord/EvoBGP/commit/dc1f4a5))
* refactor: update ListModules and GetModule methods in Postgres repository to include cron expression ([73d0003](http://192.168.100.56:3000/denozord/EvoBGP/commit/73d0003))
* refactor: update Switch component styles and structure for improved accessibility and responsiveness ([d4282c2](http://192.168.100.56:3000/denozord/EvoBGP/commit/d4282c2))
* refactor: update worker processes in EvoBGP to accept dependencies for shared store and job registry ([b7968db](http://192.168.100.56:3000/denozord/EvoBGP/commit/b7968db))
* ci: add conditional job for OpenAPI linting based on file changes; update README with workflow detai ([3adfc49](http://192.168.100.56:3000/denozord/EvoBGP/commit/3adfc49))
* ci: phase 4 automation — web job, lint-httpapi, golangci ([64516c3](http://192.168.100.56:3000/denozord/EvoBGP/commit/64516c3))
* ci: update README and CI workflow to improve bird2 job installation process and clarify Docker usage ([eddc6b8](http://192.168.100.56:3000/denozord/EvoBGP/commit/eddc6b8))
* chore: phase 3 code quality — pipeline prefixes, godoc baseline ([ea65451](http://192.168.100.56:3000/denozord/EvoBGP/commit/ea65451))
* chore: update CI configuration to disable filesystem entitlement for Docker builds ([9ab7de6](http://192.168.100.56:3000/denozord/EvoBGP/commit/9ab7de6))
* chore: update package dependencies and enhance AppShell component with new navigation items and tool ([7d85fef](http://192.168.100.56:3000/denozord/EvoBGP/commit/7d85fef))
* chore: upgrade Go version to 1.24 in Dockerfiles for evobgp-agent and gobinary. This change ensures ([4ba5b03](http://192.168.100.56:3000/denozord/EvoBGP/commit/4ba5b03))
* fix: adjust Postgres resource limits and healthcheck parameters. Increase memory limit to 256M and m ([20e674c](http://192.168.100.56:3000/denozord/EvoBGP/commit/20e674c))
* fix: improve input handling and validation for settings fields ([1386b63](http://192.168.100.56:3000/denozord/EvoBGP/commit/1386b63))
* fix: improve sidebar and navigation layout in AppShell component ([9f02128](http://192.168.100.56:3000/denozord/EvoBGP/commit/9f02128))
* fix: update BGP configuration in test scenarios to use new IPv6 addresses ([9fd9745](http://192.168.100.56:3000/denozord/EvoBGP/commit/9fd9745))
* fix: update bird build script and Dockerfile to include libncurses dependency ([0cb5a7d](http://192.168.100.56:3000/denozord/EvoBGP/commit/0cb5a7d))
* fix: update bird build script and Dockerfile to include readline dependency ([35ed79b](http://192.168.100.56:3000/denozord/EvoBGP/commit/35ed79b))
* fix: update bird build script and Dockerfile to remove readline dependency ([fefadac](http://192.168.100.56:3000/denozord/EvoBGP/commit/fefadac))
* fix: update deployment configuration for Traefik and secure Web UI ([00bd037](http://192.168.100.56:3000/denozord/EvoBGP/commit/00bd037))
* fix: update Docker build commands in CI configuration and documentation ([b5061a5](http://192.168.100.56:3000/denozord/EvoBGP/commit/b5061a5))
* fix(httpapi): phase 1 compliance — ERR-01, SEC-04, CDN client, scheduler docs ([1dd7135](http://192.168.100.56:3000/denozord/EvoBGP/commit/1dd7135))
* test: add CDN prefetch tests for 304 responses and snapshot handling ([a536a2d](http://192.168.100.56:3000/denozord/EvoBGP/commit/a536a2d))
* test: add validation for 'strict bind on' in BGP rendering tests. Enhance BGP test cases to ensure ' ([dc5e20e](http://192.168.100.56:3000/denozord/EvoBGP/commit/dc5e20e))
* docker: refactor cache handling in Docker bake configuration ([5e59a1c](http://192.168.100.56:3000/denozord/EvoBGP/commit/5e59a1c))
* docker: simplify web image targets in Docker bake configuration ([765a805](http://192.168.100.56:3000/denozord/EvoBGP/commit/765a805))
* docker: update web image build process and README ([92f6de8](http://192.168.100.56:3000/denozord/EvoBGP/commit/92f6de8))
* web(ui): app layout — sidebar, mobile sheet, topbar, PageHeader ([2d78c81](http://192.168.100.56:3000/denozord/EvoBGP/commit/2d78c81))
* web(ui): AppDataTable pattern and modules pilot ([b992301](http://192.168.100.56:3000/denozord/EvoBGP/commit/b992301))
* web(ui): design tokens — spacing and component scale ([ac635e9](http://192.168.100.56:3000/denozord/EvoBGP/commit/ac635e9))
* web(ui): feedback — confirm dialog, skeletons, README ([87309cf](http://192.168.100.56:3000/denozord/EvoBGP/commit/87309cf))
* web(ui): form patterns — FormField, formsnap, directories pilot ([77a2cfd](http://192.168.100.56:3000/denozord/EvoBGP/commit/77a2cfd))
* web(ui): foundation — ui/core layout, shadcn primitives, notify ([00a2a44](http://192.168.100.56:3000/denozord/EvoBGP/commit/00a2a44))
* web(ui): migrate dashboard, schedule, monitoring to ui patterns ([57dcf30](http://192.168.100.56:3000/denozord/EvoBGP/commit/57dcf30))
* web(ui): migrate network to ui patterns ([3a3056b](http://192.168.100.56:3000/denozord/EvoBGP/commit/3a3056b))
* web(ui): migrate operations and module detail to ui patterns ([1f9930d](http://192.168.100.56:3000/denozord/EvoBGP/commit/1f9930d))
* web(ui): migrate settings to ui patterns ([9808aca](http://192.168.100.56:3000/denozord/EvoBGP/commit/9808aca))
* Enhance EvoBGP architecture plan by refining the overview, updating todos with detailed descriptions ([30efdbf](http://192.168.100.56:3000/denozord/EvoBGP/commit/30efdbf))
* init ([cbf3aaa](http://192.168.100.56:3000/denozord/EvoBGP/commit/cbf3aaa))
* Refine EvoBGP architecture plan with updated database schema details, improved descriptions for comp ([431ef3e](http://192.168.100.56:3000/denozord/EvoBGP/commit/431ef3e))
* Update EvoBGP architecture plan with new overview and todos. Refined descriptions for control-plane ([31ff5bb](http://192.168.100.56:3000/denozord/EvoBGP/commit/31ff5bb))
+9
View File
@@ -6,6 +6,7 @@ Control plane для управления префиксами, модулями
| Документ | Содержание |
|----------|------------|
| [AGENTS.md](AGENTS.md) | Краткая карта репозитория и советы для ИИ-агентов (экономия контекста) |
| [docs/README.md](docs/README.md) | Оглавление и навигация по разделам |
| [docs/overview.md](docs/overview.md) | Ключевые возможности продукта |
| [docs/quickstart.md](docs/quickstart.md) | Быстрый запуск (Docker, локально, фронтенд) |
@@ -43,4 +44,12 @@ docker compose --profile reference up -d
- **Go:** модуль `evobgp`, точки входа в `cmd/`.
- **Веб:** SvelteKit в каталоге `web/` (см. [web/README.md](web/README.md)).
## Версионирование и релизы
Версии определяются автоматически из Conventional Commits при merge в `main` (semantic-release на Gitea Actions). Первая версия — **1.0.0**; ручной bump не нужен.
- [docs/releasing.md](docs/releasing.md) — пайплайн, commit conventions, секреты CI
- API: `GET /version` и `GET /v1/version` (поле `version`)
- Docker-образы: теги `latest`, `vX.Y.Z`, `X.Y.Z` — в **том же CI run**, что и релиз (job `release`)
Лицензия и условия использования — по политике владельца репозитория.
+5 -4
View File
@@ -19,6 +19,7 @@ import (
"evobgp/internal/platform"
"evobgp/internal/render"
"evobgp/internal/scheduler"
"evobgp/internal/version"
)
// microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
@@ -37,7 +38,7 @@ func main() {
log.Fatal(err)
}
defer srv.Close()
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
observability.SetBuildInfo(version.Version, version.GitSHA)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
@@ -51,7 +52,7 @@ func main() {
go render.Run(ctx, renderDeps)
go deploy.Run(ctx, deployDeps)
startBirdMetricsPoller()
startBirdMetricsPoller(ctx)
httpSrv := &http.Server{
Addr: cfg.HTTPAddr,
@@ -75,7 +76,7 @@ func main() {
log.Printf("%s stopped", platform.ServiceName("evobgp-all"))
}
func startBirdMetricsPoller() {
func startBirdMetricsPoller(ctx context.Context) {
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
if sock == "" {
return
@@ -85,7 +86,7 @@ func startBirdMetricsPoller() {
interval = d
}
bin := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))
observability.StartBirdProtocolsPoller(sock, bin, interval,
observability.StartBirdProtocolsPoller(ctx, sock, bin, interval,
func(ctx context.Context, socket, birdcBin string) (string, error) {
return birdfmt.ShowProtocols(ctx, socket, birdcBin)
},
+6 -5
View File
@@ -15,6 +15,7 @@ import (
"evobgp/internal/httpapi"
"evobgp/internal/observability"
"evobgp/internal/platform"
"evobgp/internal/version"
)
func main() {
@@ -34,12 +35,12 @@ func main() {
}
defer srv.Close()
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
startBirdMetricsPoller()
observability.SetBuildInfo(version.Version, version.GitSHA)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startBirdMetricsPoller(ctx)
httpSrv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: srv.Handler(),
@@ -68,7 +69,7 @@ func main() {
log.Printf("%s stopped", platform.ServiceName("evobgp-api"))
}
func startBirdMetricsPoller() {
func startBirdMetricsPoller(ctx context.Context) {
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
if sock == "" {
return
@@ -78,7 +79,7 @@ func startBirdMetricsPoller() {
interval = d
}
bin := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))
observability.StartBirdProtocolsPoller(sock, bin, interval,
observability.StartBirdProtocolsPoller(ctx, sock, bin, interval,
func(ctx context.Context, socket, birdcBin string) (string, error) {
return birdfmt.ShowProtocols(ctx, socket, birdcBin)
},
+3 -228
View File
@@ -1,240 +1,15 @@
package main
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/bundle"
"evobgp/internal/signing"
"evobgp/internal/nodecli"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "pull-bundle":
os.Exit(cmdPullBundle(os.Args[2:]))
case "verify-bundle":
os.Exit(cmdVerifyBundle(os.Args[2:]))
case "apply-bundle":
os.Exit(cmdApplyBundle(os.Args[2:]))
default:
usage()
nodecli.Usage(os.Stderr)
os.Exit(2)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage:
%s pull-bundle -base-url URL -token TOKEN -speaker-id ID [-revision-id ID] [-o path]
%s verify-bundle -f bundle.tar.gz (-pubkey-base64 B64 | -pubkey-hex HEX)
%s apply-bundle -f bundle.tar.gz -extract-dir DIR (-pubkey-base64 B64 | -pubkey-hex HEX)
[-bird PATH] [-birdc PATH] [-socket PATH] [-timeout DURATION]
apply-bundle verifies, extracts, runs bird -p on main bird.conf, then birdc configure.
`, os.Args[0], os.Args[0], os.Args[0])
}
func cmdPullBundle(args []string) int {
fs := flag.NewFlagSet("pull-bundle", flag.ExitOnError)
base := fs.String("base-url", "", "control plane base URL, e.g. http://localhost:8080")
token := fs.String("token", "", "Bearer token (node role)")
speaker := fs.String("speaker-id", "", "bgp_speaker id")
revision := fs.String("revision-id", "", "revision to fetch (empty = latest pointer)")
out := fs.String("o", "bundle.tar.gz", "output file")
_ = fs.Parse(args)
if strings.TrimSpace(*base) == "" || *token == "" || *speaker == "" {
fmt.Fprintln(os.Stderr, "pull-bundle: -base-url, -token, -speaker-id are required")
return 2
}
rev := strings.TrimSpace(*revision)
if rev == "" {
var err error
rev, err = fetchLatestRevision(*base, *token, *speaker)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
}
body, err := fetchBundle(*base, *token, *speaker, rev)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := os.WriteFile(*out, body, 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "wrote %s (revision %s)\n", *out, rev)
return 0
}
func fetchLatestRevision(base, token, speaker string) (string, error) {
u := strings.TrimRight(base, "/") + "/v1/speakers/" + speaker + "/revisions/latest"
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("latest revision: %s: %s", resp.Status, strings.TrimSpace(string(b)))
}
var out struct {
RevisionID string `json:"revision_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
if out.RevisionID == "" {
return "", fmt.Errorf("empty revision_id in response")
}
return out.RevisionID, nil
}
func fetchBundle(base, token, speaker, revision string) ([]byte, error) {
u := strings.TrimRight(base, "/") + "/v1/speakers/" + speaker + "/bundle/" + revision
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("bundle: %s: %s", resp.Status, strings.TrimSpace(string(b)))
}
return io.ReadAll(resp.Body)
}
func loadPubKey(pubB64, pubHex string) (ed25519.PublicKey, error) {
switch {
case strings.TrimSpace(pubB64) != "":
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(pubB64))
if err != nil {
return nil, err
}
if len(raw) != ed25519.PublicKeySize {
return nil, fmt.Errorf("pubkey-base64 must decode to %d bytes", ed25519.PublicKeySize)
}
return ed25519.PublicKey(raw), nil
case strings.TrimSpace(pubHex) != "":
return bundle.ParsePublicKeyHex(pubHex)
default:
return nil, fmt.Errorf("public key required")
}
}
func cmdVerifyBundle(args []string) int {
fs := flag.NewFlagSet("verify-bundle", flag.ExitOnError)
path := fs.String("f", "", "path to bundle.tar.gz")
pubB64 := fs.String("pubkey-base64", "", "Ed25519 public key (base64)")
pubHex := fs.String("pubkey-hex", "", "Ed25519 public key (64 hex chars)")
_ = fs.Parse(args)
if *path == "" {
fmt.Fprintln(os.Stderr, "verify-bundle: -f required")
return 2
}
pub, err := loadPubKey(*pubB64, *pubHex)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
raw, err := os.ReadFile(*path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
v, err := signing.VerifyGzippedTar(raw, pub)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "ok: revision %s, %d payload files\n", v.Manifest.RevisionID, len(v.Files))
return 0
}
func cmdApplyBundle(args []string) int {
fs := flag.NewFlagSet("apply-bundle", flag.ExitOnError)
path := fs.String("f", "", "path to bundle.tar.gz")
dir := fs.String("extract-dir", "", "directory to extract into")
pubB64 := fs.String("pubkey-base64", "", "Ed25519 public key (base64)")
pubHex := fs.String("pubkey-hex", "", "Ed25519 public key (64 hex chars)")
bird := fs.String("bird", "", "bird binary (default PATH)")
birdc := fs.String("birdc", "", "birdc binary (default PATH)")
socket := fs.String("socket", "", "birdc -s socket path")
timeout := fs.Duration("timeout", 30*time.Second, "timeout for bird/birdc")
_ = fs.Parse(args)
if *path == "" || *dir == "" {
fmt.Fprintln(os.Stderr, "apply-bundle: -f and -extract-dir required")
return 2
}
pub, err := loadPubKey(*pubB64, *pubHex)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
raw, err := os.ReadFile(*path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
v, err := signing.VerifyGzippedTar(raw, pub)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
root := filepath.Clean(*dir)
if err := os.MkdirAll(root, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := bundle.WriteExtractedFiles(root, v); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
mainRel := v.FindMainBirdConf()
if mainRel == "" {
fmt.Fprintln(os.Stderr, "bundle has no bird.conf path in manifest")
return 1
}
mainPath := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(mainRel, "/")))
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
ctl := &birdfmt.BirdCtl{Bird: *bird, Birdc: *birdc, Socket: *socket}
if err := ctl.ParseCheck(ctx, mainPath); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := ctl.Configure(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "applied revision %s (main config %s)\n", v.Manifest.RevisionID, mainPath)
return 0
os.Exit(nodecli.Run(os.Args[1:]))
}
+3
View File
@@ -41,6 +41,9 @@ func main() {
apiBase := strings.TrimSpace(os.Getenv("EVOBGP_CONTROL_PLANE_URL"))
apiTok := strings.TrimSpace(os.Getenv("EVOBGP_SCHEDULER_BEARER"))
if os.Getenv("EVOBGP_SCHEDULER_STANDALONE") == "1" && apiBase == "" {
log.Fatal("evobgp-scheduler: EVOBGP_SCHEDULER_STANDALONE=1 requires EVOBGP_CONTROL_PLANE_URL and EVOBGP_SCHEDULER_BEARER (split deploy must not use in-process jobs.Registry)")
}
deps := &scheduler.Deps{
Store: st,
HTTP: &http.Client{Timeout: 45 * time.Second},
+3
View File
@@ -3,3 +3,6 @@
EVOBGP_REGISTRY=git.shts.su/denozord
EVOBGP_IMAGE_TAG=latest
# Для защищенного Web UI (Traefik + Let's Encrypt + Cloudflare DNS challenge)
# используйте второй файл рядом: .env.web-sec (шаблон: .env.web-sec.example)
@@ -0,0 +1,24 @@
# Standalone stack env (Linux): stack.microvps-full.yaml
# Использование:
# cp .env.stack.microvps-full.example .env.stack.microvps-full
# docker compose --env-file .env.stack.microvps-full -f stack.microvps-full.yaml up -d
# Образы EvoBGP
EVOBGP_REGISTRY=git.shts.su/denozord
EVOBGP_IMAGE_TAG=latest
# Защищенный Web UI (Traefik + Let's Encrypt + Cloudflare DNS challenge)
WEBUI_DOMAIN=bgp.shz.su
WEBUI_IP_WHITELIST=109.174.26.78/32,87.103.241.8/32
LETSENCRYPT_EMAIL=admin@shz.su
CF_DNS_API_TOKEN=cfut_3D1i0MNBVX6MNSsjcFytsfSlyB4B15t7H3DYXuzq22011588
# Auto-updater (проверка registry образов и точечный restart контейнеров)
# 0 = выключен, 1 = включен
AUTO_UPDATE_ENABLED=0
# Интервал проверки обновлений (сек), минимум 30
AUTO_UPDATE_INTERVAL_SEC=300
# Сервисы для автообновления (через запятую)
AUTO_UPDATE_SERVICES=evobgp-all,evobgp-web
# Защищенные сервисы, которые updater никогда не перезапускает
AUTO_UPDATE_PROTECTED_SERVICES=bird2
+16
View File
@@ -0,0 +1,16 @@
# Параметры защищенного Web UI (Traefik + Let's Encrypt DNS challenge через Cloudflare).
# Используйте вместе с deploy/compose/.env:
# docker compose --env-file .env --env-file .env.web-sec ...
# FQDN для Web UI (A/AAAA запись должна указывать на VPS; Cloudflare: DNS only).
WEBUI_DOMAIN=bgp.example.com
# Разрешенные IP/CIDR (через запятую).
# Пример: один IP + подсеть офиса.
WEBUI_IP_WHITELIST=203.0.113.10/32,198.51.100.0/24
# Email для ACME аккаунта Let's Encrypt.
LETSENCRYPT_EMAIL=admin@example.com
# Cloudflare API token с правами Zone:DNS:Edit только на нужную зону.
CF_DNS_API_TOKEN=replace_me
@@ -7,6 +7,7 @@
# docker compose -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full down
#
# Лимиты под хост ~1 ГиБ: сумма лимитов контейнеров ~800 МиБ + запас ОС/Docker. При OOM увеличьте RAM VPS или уберите prometheus/nats из профиля (отдельная правка compose).
# Postgres ниже ~240–256 МиБ часто не успевает пройти healthcheck / падает по памяти — не уменьшайте postgres без tuned postgresql.conf.
services:
evobgp-all:
@@ -23,7 +24,7 @@ services:
resources:
limits:
cpus: "0.45"
memory: 180M
memory: 256M
bird2:
deploy:
+51 -3
View File
@@ -45,6 +45,8 @@ services:
profiles: ["reference", "microvps", "microvps-full"]
image: postgres:16-alpine
restart: unless-stopped
# Дефолтный /dev/shm в Docker (64 МиБ) мало для Postgres; иначе ошибки shared memory при старте.
shm_size: "256mb"
environment:
POSTGRES_USER: evobgp
POSTGRES_PASSWORD: evobgp
@@ -55,7 +57,8 @@ services:
test: ["CMD-SHELL", "pg_isready -U evobgp -d evobgp"]
interval: 10s
timeout: 5s
retries: 5
retries: 12
start_period: 45s
logging: *default-logging
deploy:
resources:
@@ -123,6 +126,8 @@ services:
condition: service_started
environment:
<<: *env-ref
# Split deploy: scheduler must enqueue jobs via API, not in-process Registry (ARCH-04).
EVOBGP_SCHEDULER_STANDALONE: "1"
EVOBGP_CONTROL_PLANE_URL: http://evobgp-api:8080
EVOBGP_SCHEDULER_BEARER: dev
logging: *default-logging
@@ -240,8 +245,15 @@ services:
restart: unless-stopped
depends_on:
- evobgp-all
ports:
- "3000:80"
labels:
- traefik.enable=true
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
- traefik.http.routers.evobgp-web.entrypoints=websecure
- traefik.http.routers.evobgp-web.tls=true
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
logging: *default-logging
deploy:
resources:
@@ -249,6 +261,41 @@ services:
cpus: "0.5"
memory: 128M
evobgp-edge:
profiles: ["microvps-full"]
image: traefik:latest
restart: unless-stopped
depends_on:
- evobgp-web-microvps
ports:
- "80:80"
- "443:443"
environment:
DOCKER_API_VERSION: "1.44"
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN}
command:
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=15
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
logging: *default-logging
deploy:
resources:
limits:
cpus: "0.35"
memory: 128M
prometheus-microvps:
profiles: ["microvps-full"]
image: prom/prometheus:v2.54.1
@@ -337,3 +384,4 @@ volumes:
pgdata:
bird_etc:
bird_run:
traefik_letsencrypt:
+342
View File
@@ -0,0 +1,342 @@
# Standalone stack (без profiles): Portainer / `docker compose -f stack.microvps-full.yaml up -d`
# Эквивалент microvps-full из docker-compose.yaml + docker-compose.microvps-full.yaml,
# с встроенными configs для Prometheus (как у вас).
#
# Postgres: shm_size + start_period — иначе часто «dependency postgres failed» на слабом хосте.
# Для защищенного Web UI задайте переменные окружения перед запуском:
# WEBUI_DOMAIN, WEBUI_IP_WHITELIST, LETSENCRYPT_EMAIL, CF_DNS_API_TOKEN
# Cloudflare запись для WEBUI_DOMAIN: DNS only (серый облачок).
#
# ACME / Let's Encrypt: путь задан в evobgp-edge (--certificatesresolvers...storage).
# Имя тома traefik_letsencrypt зафиксировано (volumes.traefik_letsencrypt.name), иначе при
# другом имени стека в Portainer или смене project name создаётся новый пустой том —
# Traefik не находит acme.json и отдаёт дефолтный сертификат до новой выдачи LE.
# Не использовать `docker compose down -v` без бэкапа. Если раньше был том с префиксом
# проекта, перенесите acme.json в том evobgp_traefik_letsencrypt.
#
# Долгий сбор логов в файлы на хосте: сервис stack-runtime-logs пишет в каталог
# ./runtime-logs/ (рядом с этим compose-файлом) по одному файлу на сервис.
# Автообновление сервисов из registry (без перезапуска bird2): stack-auto-updater.
# Настройки через .env: AUTO_UPDATE_ENABLED, AUTO_UPDATE_INTERVAL_SEC,
# AUTO_UPDATE_SERVICES, AUTO_UPDATE_PROTECTED_SERVICES.
# Имя проекта в Docker должно совпадать с label com.docker.compose.project:
# при смене `name:` или имени стека в Portainer задайте COMPOSE_PROJECT_NAME.
# Требуется доступ к docker.sock (полные права на демон — осознанно).
name: evobgp-microvps-full
configs:
prometheus_yml:
content: |
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: evobgp-all
metrics_path: /metrics
static_configs:
- targets: ["evobgp-all:8080"]
prometheus_alerts_yml:
content: |
groups: []
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
shm_size: "256mb"
environment:
POSTGRES_USER: evobgp
POSTGRES_PASSWORD: evobgp
POSTGRES_DB: evobgp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U evobgp -d evobgp"]
interval: 10s
timeout: 5s
retries: 12
start_period: 45s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
nats:
image: nats:2.10-alpine
restart: unless-stopped
command: ["-js", "-m", "8222"]
ports:
- "4222:4222"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
bird2:
image: ${EVOBGP_REGISTRY:-git.shts.su/denozord}/evobgp-bird2:${EVOBGP_IMAGE_TAG:-latest}
restart: unless-stopped
cap_add:
- NET_ADMIN
sysctls:
net.ipv4.ip_forward: "1"
net.ipv6.conf.all.forwarding: "1"
volumes:
- bird_etc:/etc/bird
- bird_run:/run/bird
ports:
- "179:179/tcp"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
evobgp-agent:
image: ${EVOBGP_REGISTRY:-git.shts.su/denozord}/evobgp-agent:${EVOBGP_IMAGE_TAG:-latest}
restart: unless-stopped
depends_on:
bird2:
condition: service_started
cap_add:
- NET_ADMIN
volumes:
- bird_etc:/etc/bird
- bird_run:/run/bird
entrypoint: ["/usr/local/bin/evobgp-agent"]
command: ["watch", "-socket=/run/bird/bird.ctl", "-watch-interval=30s"]
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
evobgp-all:
image: ${EVOBGP_REGISTRY:-git.shts.su/denozord}/evobgp-all:${EVOBGP_IMAGE_TAG:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
nats:
condition: service_started
bird2:
condition: service_started
ports:
- "8080:8080"
environment:
EVOBGP_DATABASE_URL: postgres://evobgp:evobgp@postgres:5432/evobgp?sslmode=disable
EVOBGP_BROKER_URL: nats://nats:4222
EVOBGP_HTTP_ADDR: ":8080"
EVOBGP_SEED_DEMO: "1"
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
EVOBGP_BIRDC_INTERVAL: 30s
EVOBGP_BIRD_ACTIVE_DIR: /etc/bird
EVOBGP_BIRD_STAGING_DIR: /tmp/evobgp-bird-staging
EVOBGP_DEV_INSECURE: "1"
volumes:
- bird_etc:/etc/bird
- bird_run:/run/bird:ro
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
evobgp-web:
image: ${EVOBGP_REGISTRY:-git.shts.su/denozord}/evobgp-web-all:${EVOBGP_IMAGE_TAG:-latest}
restart: unless-stopped
depends_on:
evobgp-all:
condition: service_started
labels:
- traefik.enable=true
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
- traefik.http.routers.evobgp-web.entrypoints=websecure
- traefik.http.routers.evobgp-web.tls=true
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
evobgp-edge:
image: traefik:latest
restart: unless-stopped
depends_on:
evobgp-web:
condition: service_started
ports:
- "80:80"
- "443:443"
environment:
DOCKER_API_VERSION: "1.44"
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN}
command:
- --log.level=INFO
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=15
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
prometheus:
image: prom/prometheus:v2.54.1
restart: unless-stopped
depends_on:
evobgp-all:
condition: service_started
ports:
- "9090:9090"
configs:
- source: prometheus_yml
target: /etc/prometheus/prometheus.yml
- source: prometheus_alerts_yml
target: /etc/prometheus/alerts.yml
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=7d
- --web.enable-lifecycle
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
stack-runtime-logs:
image: docker:27-cli
restart: unless-stopped
depends_on:
evobgp-all:
condition: service_started
environment:
COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-evobgp-microvps-full}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- type: bind
source: ./runtime-logs
target: /logs
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
mkdir -p /logs
PROJECT=$$COMPOSE_PROJECT_NAME
SERVICES="postgres nats bird2 evobgp-agent evobgp-all evobgp-web evobgp-edge prometheus"
log_one() {
svc=$$1
f="/logs/$$svc.log"
while true; do
cid=$$(docker ps -q \
-f "label=com.docker.compose.service=$$svc" \
-f "label=com.docker.compose.project=$$PROJECT" | head -n1)
if [ -n "$$cid" ]; then
echo "---- $$(date -u +"%Y-%m-%dT%H:%M:%SZ") attach $$svc $$cid ----" >> "$$f"
docker logs -f --timestamps "$$cid" >> "$$f" 2>&1 || true
fi
sleep 3
done
}
for s in $$SERVICES; do log_one "$$s" & done
wait
stack-auto-updater:
image: docker:27-cli
restart: unless-stopped
depends_on:
evobgp-all:
condition: service_started
environment:
COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-evobgp-microvps-full}
AUTO_UPDATE_ENABLED: ${AUTO_UPDATE_ENABLED:-0}
AUTO_UPDATE_INTERVAL_SEC: ${AUTO_UPDATE_INTERVAL_SEC:-300}
AUTO_UPDATE_SERVICES: ${AUTO_UPDATE_SERVICES:-evobgp-all,evobgp-web}
AUTO_UPDATE_PROTECTED_SERVICES: ${AUTO_UPDATE_PROTECTED_SERVICES:-bird2}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- type: bind
source: ./stack.microvps-full.yaml
target: /stack/stack.microvps-full.yaml
read_only: true
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
PROJECT=$$COMPOSE_PROJECT_NAME
INTERVAL=$$AUTO_UPDATE_INTERVAL_SEC
ENABLED=$$AUTO_UPDATE_ENABLED
if [ "$$ENABLED" != "1" ]; then
echo "auto-updater disabled: AUTO_UPDATE_ENABLED=$$ENABLED"
while true; do sleep 3600; done
fi
if [ "$$INTERVAL" -lt 30 ]; then
echo "auto-updater interval too low ($$INTERVAL), forcing to 30"
INTERVAL=30
fi
echo "auto-updater started: project=$$PROJECT interval=$$INTERVAL services=$$AUTO_UPDATE_SERVICES protected=$$AUTO_UPDATE_PROTECTED_SERVICES"
while true; do
TS=$$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SERVICES_LIST=$$(echo "$$AUTO_UPDATE_SERVICES" | tr ',' ' ')
PROTECTED_LIST=$$(echo "$$AUTO_UPDATE_PROTECTED_SERVICES" | tr ',' ' ')
for svc in $$SERVICES_LIST; do
if [ -z "$$svc" ]; then
continue
fi
skip=0
for protected in $$PROTECTED_LIST; do
if [ "$$svc" = "$$protected" ]; then
echo "$$TS skip protected service '$$svc'"
skip=1
break
fi
done
if [ "$$skip" = "1" ]; then
continue
fi
before=$$(docker image inspect "$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml config --images $$svc | tail -n1)" --format '{{.Id}}' 2>/dev/null || true)
pull_out=$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml pull $$svc 2>&1 || true)
echo "$$TS pull $$svc: $$pull_out"
after=$$(docker image inspect "$$(docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml config --images $$svc | tail -n1)" --format '{{.Id}}' 2>/dev/null || true)
if [ -n "$$after" ] && [ "$$before" != "$$after" ]; then
echo "$$TS update detected for $$svc, restarting container"
docker compose -p "$$PROJECT" -f /stack/stack.microvps-full.yaml up -d --no-deps $$svc
else
echo "$$TS no update for $$svc"
fi
done
sleep "$$INTERVAL"
done
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
pgdata:
bird_etc:
bird_run:
traefik_letsencrypt:
name: evobgp_traefik_letsencrypt
+56
View File
@@ -0,0 +1,56 @@
# Docker-образы EvoBGP
## CI (Gitea Actions)
Сборка образов — **`docker buildx bake`** (`deploy/docker/docker-bake.hcl`), не отдельные `docker build`.
**Publish:** push в `main` после quality gates — job **release** в [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml): semantic-release + bake с `VERSION` из релиза.
Локально BuildKit также кэширует `/go/pkg/mod` и `~/.cache/go-build` через `RUN --mount=type=cache`.
| Было | Стало |
|------|--------|
| 8× `go mod download` + 8× `go build` (разные BIN) | 1× download + 1× компиляция всех `cmd/*` |
| 2× сборка BIRD из исходников (api, all) | 1× stage `birdc`, копируется в runtime |
| 2× `npm ci` (web, web-all) | 1× `web-deps` + 1× `web-build` + два nginx-образа (общий артефакт) |
| 8 runner'ов с checkout/login | 1 job `docker-go`, 1 job `docker-web` |
Кэш registry (переменные bake):
- `git.shts.su/<owner>/evobgp-buildcache:go-buildcache` — **запись** только из target `go-build-all`
- `git.shts.su/<owner>/evobgp-buildcache:web-buildcache` — **запись** только из target `web-build`
Остальные bake-target’ы только `cache-from` (чтение). Параллельный `cache-to` в один ref ломает manifest в registry (`content descriptor … not found`).
Локально BuildKit также кэширует `/go/pkg/mod` и `~/.cache/go-build` через `RUN --mount=type=cache`.
Если CI падает на «not found» после смены схемы кэша — один раз удалите теги `evobgp-buildcache:go-buildcache` и `:web-buildcache` в registry и пересоберите.
## Локальная сборка
Из корня репозитория:
```bash
cd deploy/docker
export REGISTRY=git.shts.su/<owner>
export IMAGE_TAG=latest
export SHORT_SHA=$(git rev-parse --short HEAD)
export VERSION=dev
export BUILD_TIME=
sh write-bake-override.sh
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl go-images
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl web-images
```
В `docker-bake.hcl`: `context = "../.."` (корень репо), `dockerfile = "deploy/docker/…"` (путь от корня репо). **CI:** `write-bake-override.sh` + `--allow=fs.read=$GITHUB_WORKSPACE` в `.gitea/workflows/ci.yaml`.
Один образ (legacy):
```bash
docker build -f deploy/docker/gobinary/Dockerfile \
--target build-all \
--build-arg BIN=evobgp-api \
-t evobgp-api:local .
```
Runtime-образы в bake ожидают stage `build-all` (через bake contexts), не отдельный `--build-arg BIN` на старый target `build`.
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Сборка BIRD из официального tarball (версия задаётся BIRD_VERSION, по умолчанию 2.14).
# Запуск от root (Docker) или через sudo на хосте. Префикс установки: /usr/local.
# birdc требует ncurses + readline; в runtime-образе gobinary — libncurses6 + libreadline8.
set -eu
BIRD_VERSION="${BIRD_VERSION:-2.14}"
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
flex \
bison \
libncurses-dev \
libreadline-dev
cd /tmp
rm -rf "bird-${BIRD_VERSION}"
curl -fsSL "https://bird.network.cz/download/bird-${BIRD_VERSION}.tar.gz" | tar xz
cd "bird-${BIRD_VERSION}"
./configure --prefix=/usr/local --enable-client
make -j"$(nproc)"
make install
cd /
rm -rf "/tmp/bird-${BIRD_VERSION}"
apt-get purge -y build-essential flex bison libncurses-dev libreadline-dev
apt-get autoremove -y
rm -rf /var/lib/apt/lists/*
+7 -5
View File
@@ -1,9 +1,11 @@
# BIRD 2 alongside evobgp-agent; share /etc/bird with the agent container (plan §4.1).
FROM public.ecr.aws/docker/library/debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends bird2 \
&& rm -rf /var/lib/apt/lists/*
# BIRD из репозитория Ubuntu (Noble 24.04 LTS) — актуальнее пакета Debian bookworm.
# Сборка из корня репозитория: docker build -f deploy/docker/bird2/Dockerfile .
# Зеркало ECR Public вместо прямого pull с Docker Hub.
FROM public.ecr.aws/docker/library/ubuntu:noble
RUN apt-get update \
&& apt-get install -y --no-install-recommends bird2 \
&& rm -rf /var/lib/apt/lists/*
COPY deploy/bird/bird.conf /etc/bird/bird.conf
RUN mkdir -p /etc/bird/bird.d
EXPOSE 179
# Foreground for Docker; configs on shared volume override this file at runtime.
CMD ["bird", "-f", "-c", "/etc/bird/bird.conf"]
+265
View File
@@ -0,0 +1,265 @@
# Единая сборка образов EvoBGP (buildx bake: -f deploy/docker/docker-bake.hcl).
# context = "../.." — корень репозитория (относительно этого файла).
# dockerfile — путь от корня репозитория (относительно context).
# Переменные: REGISTRY, IMAGE_TAG, CACHE_REF_GO, CACHE_REF_WEB
variable "REGISTRY" {
default = "git.shts.su/evobgp"
}
variable "IMAGE_TAG" {
default = "latest"
}
variable "SHORT_SHA" {
default = "dev"
}
variable "SHA_FULL" {
default = ""
}
variable "VERSION" {
default = "dev"
}
variable "BUILD_TIME" {
default = ""
}
variable "CACHE_REF_GO" {
default = ""
}
variable "CACHE_REF_WEB" {
default = ""
}
function "go-cache-from" {
params = []
result = notequal("", CACHE_REF_GO) ? ["type=registry,ref=${CACHE_REF_GO}"] : []
}
# Экспорт кэша — только go-build-all / web-build (один writer на ref).
# Несколько target с cache-to в один ref и mode=max дают гонку в registry
# (content descriptor not found при параллельном bake).
function "go-cache-to-export" {
params = []
result = notequal("", CACHE_REF_GO) ? ["type=registry,ref=${CACHE_REF_GO},mode=max"] : []
}
function "web-cache-from" {
params = []
result = notequal("", CACHE_REF_WEB) ? ["type=registry,ref=${CACHE_REF_WEB}"] : []
}
function "web-cache-to-export" {
params = []
result = notequal("", CACHE_REF_WEB) ? ["type=registry,ref=${CACHE_REF_WEB},mode=max"] : []
}
group "default" {
targets = ["go-images", "web-images", "evobgp-bird2"]
}
group "go-images" {
targets = [
"evobgp-api",
"evobgp-all",
"evobgp-scheduler",
"evobgp-ingest",
"evobgp-render",
"evobgp-deploy",
"evobgp-node",
"evobgp-agent",
]
}
# web-deps / web-build — только зависимости (contexts), без tags; при --push в группе не указывать.
group "web-images" {
targets = ["evobgp-web", "evobgp-web-all"]
}
# --- Go: go mod download → все cmd/* → birdc (один раз) → runtime-образы ---
target "go-deps" {
context = "../.."
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "deps"
platforms = ["linux/amd64"]
cache-from = go-cache-from()
}
target "go-build-all" {
context = "../.."
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "build-all"
platforms = ["linux/amd64"]
args = {
VERSION = VERSION
GIT_SHA = notequal("", SHA_FULL) ? SHA_FULL : SHORT_SHA
BUILD_TIME = BUILD_TIME
}
contexts = {
deps = "target:go-deps"
}
cache-from = go-cache-from()
cache-to = go-cache-to-export()
}
target "go-birdc" {
context = "../.."
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "birdc"
platforms = ["linux/amd64"]
cache-from = go-cache-from()
}
target "_go-runtime" {
context = "../.."
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "runtime"
platforms = ["linux/amd64"]
contexts = {
build-all = "target:go-build-all"
}
cache-from = go-cache-from()
}
target "_go-runtime-birdc" {
context = "../.."
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "runtime-birdc"
platforms = ["linux/amd64"]
contexts = {
build-all = "target:go-build-all"
birdc = "target:go-birdc"
}
cache-from = go-cache-from()
}
function "image-tags" {
params = [name]
result = concat(
notequal("", SHA_FULL) ? [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
"${REGISTRY}/${name}:sha-${SHA_FULL}",
] : [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
],
notequal(VERSION, "dev") && notequal(VERSION, "") ? [
"${REGISTRY}/${name}:v${VERSION}",
"${REGISTRY}/${name}:${VERSION}",
] : []
)
}
target "evobgp-api" {
inherits = ["_go-runtime-birdc"]
args = { BIN = "evobgp-api" }
tags = image-tags("evobgp-api")
}
target "evobgp-all" {
inherits = ["_go-runtime-birdc"]
args = { BIN = "evobgp-all" }
tags = image-tags("evobgp-all")
}
target "evobgp-scheduler" {
inherits = ["_go-runtime"]
args = { BIN = "evobgp-scheduler" }
tags = image-tags("evobgp-scheduler")
}
target "evobgp-ingest" {
inherits = ["_go-runtime"]
args = { BIN = "evobgp-ingest" }
tags = image-tags("evobgp-ingest")
}
target "evobgp-render" {
inherits = ["_go-runtime"]
args = { BIN = "evobgp-render" }
tags = image-tags("evobgp-render")
}
target "evobgp-deploy" {
inherits = ["_go-runtime"]
args = { BIN = "evobgp-deploy" }
tags = image-tags("evobgp-deploy")
}
target "evobgp-node" {
inherits = ["_go-runtime"]
args = { BIN = "evobgp-node" }
tags = image-tags("evobgp-node")
}
target "evobgp-agent" {
context = "../.."
dockerfile = "deploy/docker/evobgp-agent/Dockerfile"
platforms = ["linux/amd64"]
contexts = {
build-all = "target:go-build-all"
}
cache-from = go-cache-from()
tags = image-tags("evobgp-agent")
}
# --- Web: npm ci (кэш) → build → nginx ---
target "web-deps" {
context = "../.."
dockerfile = "deploy/docker/evobgp-web/Dockerfile"
target = "deps"
platforms = ["linux/amd64"]
cache-from = web-cache-from()
}
target "web-build" {
context = "../.."
dockerfile = "deploy/docker/evobgp-web/Dockerfile"
target = "build"
platforms = ["linux/amd64"]
contexts = {
deps = "target:web-deps"
}
cache-from = web-cache-from()
cache-to = web-cache-to-export()
}
target "evobgp-web" {
context = "../.."
dockerfile = "deploy/docker/evobgp-web/Dockerfile"
target = "web"
platforms = ["linux/amd64"]
contexts = {
web-artifacts = "target:web-build"
}
args = { EVOBGP_UPSTREAM = "evobgp-api" }
cache-from = web-cache-from()
tags = image-tags("evobgp-web")
}
target "evobgp-web-all" {
context = "../.."
dockerfile = "deploy/docker/evobgp-web/Dockerfile"
target = "web"
platforms = ["linux/amd64"]
contexts = {
web-artifacts = "target:web-build"
}
args = { EVOBGP_UPSTREAM = "evobgp-all" }
cache-from = web-cache-from()
tags = image-tags("evobgp-web-all")
}
target "evobgp-bird2" {
context = "../.."
dockerfile = "deploy/docker/bird2/Dockerfile"
platforms = ["linux/amd64"]
tags = image-tags("evobgp-bird2")
}
+7 -11
View File
@@ -1,14 +1,10 @@
# evobgp-agent CLI (parse-check, birdc configure); install bird2 so `bird -p` matches bird2 image (plan §13).
# См. gobinary/Dockerfile — ECR Public вместо прямого pull с Docker Hub (устойчивость CI).
FROM public.ecr.aws/docker/library/golang:1.22-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o /out/evobgp-agent ./cmd/evobgp-agent
FROM public.ecr.aws/docker/library/debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends bird2 ca-certificates \
# syntax=docker/dockerfile:1.7
# Агент: бинарь из общего build-all (docker-bake.hcl → contexts.build-all), bird2 из apt.
FROM public.ecr.aws/docker/library/ubuntu:noble
RUN apt-get update \
&& apt-get install -y --no-install-recommends bird2 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/evobgp-agent /usr/local/bin/evobgp-agent
ARG BIN=evobgp-agent
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp-agent
WORKDIR /etc/bird
ENTRYPOINT ["/usr/local/bin/evobgp-agent"]
+12 -7
View File
@@ -1,14 +1,19 @@
# Статическая панель EvoBGP (SvelteKit) + nginx как reverse-proxy к evobgp-api (/v1, /metrics).
# ECR Public — то же содержимое, что library/node и library/nginx на Docker Hub.
FROM public.ecr.aws/docker/library/node:22-alpine AS build
# syntax=docker/dockerfile:1.7
# Статическая панель EvoBGP (SvelteKit) + nginx.
# Финальный stage `web` ожидает bake-контекст web-artifacts (= target:web-build).
FROM public.ecr.aws/docker/library/node:22-alpine AS deps
WORKDIR /web
COPY web/package.json web/package-lock.json ./
RUN npm ci
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci
FROM deps AS build
COPY web/ ./
RUN npm run build
FROM public.ecr.aws/docker/library/nginx:1.27-alpine
FROM public.ecr.aws/docker/library/nginx:1.27-alpine AS web
ARG EVOBGP_UPSTREAM=evobgp-api
COPY deploy/docker/evobgp-web/nginx.conf /tmp/nginx-default.conf
RUN sed -e "s/evobgp-api/${EVOBGP_UPSTREAM}/g" /tmp/nginx-default.conf > /etc/nginx/conf.d/default.conf && rm -f /tmp/nginx-default.conf
COPY --from=build /web/build /usr/share/nginx/html
RUN sed -e "s/evobgp-api/${EVOBGP_UPSTREAM}/g" /tmp/nginx-default.conf > /etc/nginx/conf.d/default.conf \
&& rm -f /tmp/nginx-default.conf
COPY --from=web-artifacts /web/build /usr/share/nginx/html
+62 -12
View File
@@ -1,19 +1,69 @@
# Универсальная сборка бинаря из cmd/* (ARG BIN=evobgp-api | evobgp-all).
# INSTALL_BIRDC=1 ставит bird2 в рантайм-образ для EVOBGP_BIRDC_SOCKET (poll метрик birdc).
# Базовые образы из ECR Public (официальное зеркало library/*), чтобы CI не зависел от auth.docker.io.
FROM public.ecr.aws/docker/library/golang:1.22-bookworm AS build
# syntax=docker/dockerfile:1.7
# Универсальная сборка бинарей cmd/* (ARG BIN) или всех сразу (target build-all).
# INSTALL_BIRDC=1 — birdc из stage birdc (собирается один раз, переиспользуется api/all).
# CI: docker buildx bake -f deploy/docker/docker-bake.hcl
FROM public.ecr.aws/docker/library/golang:1.24-bookworm AS deps
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
go mod download
FROM deps AS build-all
COPY . .
ARG VERSION=dev
ARG GIT_SHA=unknown
ARG BUILD_TIME=
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
for d in cmd/*/; do \
name="$(basename "$d")"; \
CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w \
-X evobgp/internal/version.Version=${VERSION} \
-X evobgp/internal/version.GitSHA=${GIT_SHA} \
-X evobgp/internal/version.BuildTime=${BUILD_TIME}" \
-o "/out/${name}" "./cmd/${name}"; \
done
# Один бинарь (локальная сборка); в CI — build-all + runtime.
FROM deps AS build
COPY . .
ARG BIN=evobgp-api
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/evobgp ./cmd/${BIN}
ARG VERSION=dev
ARG GIT_SHA=unknown
ARG BUILD_TIME=
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w \
-X evobgp/internal/version.Version=${VERSION} \
-X evobgp/internal/version.GitSHA=${GIT_SHA} \
-X evobgp/internal/version.BuildTime=${BUILD_TIME}" \
-o /out/evobgp "./cmd/${BIN}"
FROM public.ecr.aws/docker/library/debian:bookworm-slim
ARG INSTALL_BIRDC=0
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& if [ "$INSTALL_BIRDC" = "1" ]; then apt-get install -y --no-install-recommends bird2; fi \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/evobgp /usr/local/bin/evobgp
FROM public.ecr.aws/docker/library/debian:bookworm-slim AS birdc
ARG BIRD_VERSION=2.14
COPY deploy/docker/bird/bird-from-source.sh /tmp/bird-from-source.sh
RUN chmod +x /tmp/bird-from-source.sh \
&& BIRD_VERSION="${BIRD_VERSION}" /tmp/bird-from-source.sh \
&& rm -f /tmp/bird-from-source.sh
FROM public.ecr.aws/docker/library/debian:bookworm-slim AS runtime-base
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
FROM runtime-base AS runtime
ARG BIN=evobgp-api
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/evobgp"]
FROM runtime AS runtime-birdc
# birdc: динамическая линковка readline + ncurses (debian bookworm).
RUN apt-get update \
&& apt-get install -y --no-install-recommends libreadline8 libncurses6 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=birdc /usr/local/sbin/bird /usr/local/sbin/birdc /usr/local/sbin/
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env sh
# Writes docker-bake.override.hcl for CI (buildx without --var support).
set -eu
OUT="${1:-docker-bake.override.hcl}"
REGISTRY="${REGISTRY:?REGISTRY required}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
SHORT_SHA="${SHORT_SHA:-dev}"
SHA_FULL="${SHA_FULL:-}"
VERSION="${VERSION:-dev}"
BUILD_TIME="${BUILD_TIME:-}"
CACHE_REF_GO="${CACHE_REF_GO:-}"
CACHE_REF_WEB="${CACHE_REF_WEB:-}"
cat >"$OUT" <<EOF
# Generated by deploy/docker/write-bake-override.sh — do not commit.
variable "REGISTRY" {
default = "${REGISTRY}"
}
variable "IMAGE_TAG" {
default = "${IMAGE_TAG}"
}
variable "SHORT_SHA" {
default = "${SHORT_SHA}"
}
variable "SHA_FULL" {
default = "${SHA_FULL}"
}
variable "VERSION" {
default = "${VERSION}"
}
variable "BUILD_TIME" {
default = "${BUILD_TIME}"
}
variable "CACHE_REF_GO" {
default = "${CACHE_REF_GO}"
}
variable "CACHE_REF_WEB" {
default = "${CACHE_REF_WEB}"
}
EOF
+18
View File
@@ -4,6 +4,7 @@
## По роли читателя
- **ИИ-агент / ассистент в репозитории** — [../AGENTS.md](../AGENTS.md): с чего начать чтение, карта `internal/` и `cmd/`, что не тащить в контекст. **Инженерные правила:** [../.cursor/rules/engineering.mdc](../.cursor/rules/engineering.mdc) (общие), [../.cursor/rules/web-shadcn.mdc](../.cursor/rules/web-shadcn.mdc) (Web UI), [../.cursor/rules/networking-bird.mdc](../.cursor/rules/networking-bird.mdc) (BIRD2/BGP/IP).
- **Оператор / DevOps** — [quickstart.md](quickstart.md), [architecture.md](architecture.md), [access.md](access.md), [deploy/compose/docker-compose.yaml](../deploy/compose/docker-compose.yaml).
- **Разработчик бэкенда или интегратор API** — [api.md](api.md), [access.md](access.md), [openapi.yaml](openapi.yaml), исходники маршрутов в `internal/httpapi/`.
- **Разработчик фронтенда** — [quickstart.md](quickstart.md) (раздел про `web/` и CORS), [api.md](api.md), [../web/README.md](../web/README.md).
@@ -15,12 +16,29 @@
| [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 |
| [router-lists-ui-integration.md](router-lists-ui-integration.md) | Интеграция `router-lists-ui` с EvoBGP API (`DOMAINS/IP_RANGES/AS_PREFIXES/communities`) |
| [access.md](access.md) | Выдача доступа: API-ключи, роли, нода, CORS |
| [releasing.md](releasing.md) | Автоматические релизы, Conventional Commits, CI |
| [openapi.yaml](openapi.yaml) | Источник правды по контракту API |
| [OPENAPI-GITEA.md](OPENAPI-GITEA.md) | Как открыть HTML-документацию API (в т.ч. из Gitea) |
| [evobgp-api-sketches.md](evobgp-api-sketches.md) | Ранний черновик идей API (контекст, не замена OpenAPI) |
## Инженерные правила (Cursor / разработка)
| Файл | Область |
|------|---------|
| [../.cursor/rules/engineering.mdc](../.cursor/rules/engineering.mdc) | Go, API, migrations, security, enforcement |
| [../.cursor/rules/web-shadcn.mdc](../.cursor/rules/web-shadcn.mdc) | SvelteKit, shadcn-svelte |
| [../.cursor/rules/networking-bird.mdc](../.cursor/rules/networking-bird.mdc) | BIRD2, BGP policy, IP/CIDR |
| [../.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc) | Conventional Commits (заголовок EN, тело RU) |
### Сообщения коммитов (Cursor)
После `git add` попросите агента: **«сгенерируй коммит по staged»**, **«закоммить»**, **«commit message»** — агент **обязан первым делом** запустить `scripts/commit/staged-context.ps1`, затем скилл [commit-message](../.cursor/skills/commit-message/SKILL.md) (заголовок EN, тело RU, auto-split). Просмотр групп вручную: `powershell -NoProfile -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`.
## Репозиторий и CI
- [../.gitea/README.md](../.gitea/README.md) — Gitea Actions, runner, сборка образов и push в Container Registry.
- [releasing.md](releasing.md) — semantic-release, commit conventions, теги образов.
+1
View File
@@ -48,6 +48,7 @@
- `GET /v1/peers`, `POST /v1/peers`
- `GET|PATCH|DELETE /v1/peers/{id}`
- Для `POST|PATCH|DELETE` peer запускается быстрый job `peer_reconcile` (без module ingest/сбора префиксов); после него автоматически ставится apply на спикеры.
### Speakers
+1 -1
View File
@@ -59,7 +59,7 @@
| Метод | Путь | Описание |
|-------|------|----------|
| `GET` | `/v1/modules/{module_id}/as-entries` | Список ASN/префиксов. |
| `GET` | `/v1/modules/{module_id}/as-entries` | Список записей: ASN + community. |
| `POST` | `/v1/modules/{module_id}/as-entries` | Добавить. |
| `PATCH` | `/v1/modules/{module_id}/as-entries/{entry_id}` | Обновить. |
| `DELETE` | `/v1/modules/{module_id}/as-entries/{entry_id}` | Удалить. |
+142
View File
@@ -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`
+383 -21
View File
@@ -1,10 +1,12 @@
openapi: 3.1.0
info:
title: EvoBGP Control Plane API
version: 0.1.0
version: 1.0.0
description: |
REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).
**Актуальная semver-сборка:** `GET /version` или `GET /v1/version` (поле `version`; совпадает с git-тегом `vX.Y.Z`).
**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.
Ошибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
Пагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.
@@ -185,7 +187,9 @@ components:
in: query
schema:
type: string
description: Фильтр по виду задачи; точный перечень расширяем.
description: >
Фильтр по виду задачи; точный перечень расширяем.
Основные значения: `module_refresh`, `peer_reconcile`, `deploy_apply`, `revision_rollback`, `bird_reload`.
responses:
Unauthorized:
@@ -293,6 +297,18 @@ components:
- DOMAINS
- IP_RANGES
DohResolverPolicy:
type: string
description: |
Политика резолва доменов через DoH при нескольких профилях.
`primary_only` — только первый профиль; `failover` — по порядку до первого успешного;
`union` — объединение A/AAAA со всех профилей.
enum:
- primary_only
- failover
- union
default: primary_only
JobStatus:
type: string
description: Статус задачи; перечень может расширяться.
@@ -330,6 +346,14 @@ components:
type: integer
doh_profile_id:
type: ["string", "null"]
description: Первый DoH-профиль (legacy); предпочтительно `doh_profile_ids`.
doh_profile_ids:
type: array
items:
$ref: "#/components/schemas/ResourceId"
description: Упорядоченный список DoH-профилей для модулей `DOMAINS`.
doh_resolver_policy:
$ref: "#/components/schemas/DohResolverPolicy"
refresh_interval_sec:
type: ["integer", "null"]
minimum: 0
@@ -337,6 +361,10 @@ components:
type: ["string", "null"]
default_community_id:
type: ["string", "null"]
last_refreshed_at:
type: ["string", "null"]
format: date-time
description: Время последнего успешного обновления данных модуля.
additionalProperties: true
ModuleCreate:
@@ -357,8 +385,20 @@ components:
default: 0
doh_profile_id:
type: ["string", "null"]
description: Первый DoH-профиль (legacy); предпочтительно `doh_profile_ids`.
doh_profile_ids:
type: array
items:
$ref: "#/components/schemas/ResourceId"
description: Упорядоченный список DoH-профилей для модулей `DOMAINS`.
doh_resolver_policy:
$ref: "#/components/schemas/DohResolverPolicy"
refresh_interval_sec:
type: ["integer", "null"]
last_refreshed_at:
type: ["string", "null"]
format: date-time
description: Время последнего успешного обновления этого CDN-источника.
cron_expr:
type: ["string", "null"]
default_community_id:
@@ -375,6 +415,14 @@ components:
type: integer
doh_profile_id:
type: ["string", "null"]
description: Первый DoH-профиль (legacy); предпочтительно `doh_profile_ids`.
doh_profile_ids:
type: array
items:
$ref: "#/components/schemas/ResourceId"
description: Упорядоченный список DoH-профилей для модулей `DOMAINS`.
doh_resolver_policy:
$ref: "#/components/schemas/DohResolverPolicy"
refresh_interval_sec:
type: ["integer", "null"]
cron_expr:
@@ -397,6 +445,9 @@ components:
source_kind:
type: string
description: Формат скачанного списка / парсер.
prefix_path:
type: string
description: Путь до поля с префиксами для source_kind=json (например data.items[].cidr).
community_id:
type: ["string", "null"]
refresh_interval_sec:
@@ -413,6 +464,8 @@ components:
format: uri
source_kind:
type: string
prefix_path:
type: string
community_id:
type: ["string", "null"]
@@ -424,34 +477,76 @@ components:
format: uri
source_kind:
type: string
prefix_path:
type: string
community_id:
type: ["string", "null"]
refresh_interval_sec:
type: ["integer", "null"]
CdnPreviewRequest:
type: object
required: [url, source_kind]
properties:
url:
type: string
format: uri
source_kind:
type: string
prefix_path:
type: string
CdnPreviewResponse:
type: object
required: [items, total, truncated, source_url]
properties:
items:
type: array
items:
type: string
total:
type: integer
truncated:
type: boolean
source_url:
type: string
AsEntry:
type: object
required:
- id
- asn
properties:
id:
$ref: "#/components/schemas/ResourceId"
asn:
type: ["integer", "null"]
prefix:
type: ["string", "null"]
description: CIDR или префикс в зависимости от модели.
type: integer
minimum: 1
maximum: 4294967295
community_id:
type: ["string", "null"]
additionalProperties: true
AsEntryCreate:
type: object
required:
- asn
properties:
asn:
type: integer
prefix:
type: string
minimum: 1
maximum: 4294967295
community_id:
type: ["string", "null"]
additionalProperties: true
AsEntryPatch:
type: object
properties:
asn:
type: integer
minimum: 1
maximum: 4294967295
community_id:
type: ["string", "null"]
additionalProperties: true
@@ -513,6 +608,9 @@ components:
properties:
id:
$ref: "#/components/schemas/ResourceId"
name:
type: string
description: Отображаемое имя профиля (опционально).
url:
type: string
format: uri
@@ -528,6 +626,9 @@ components:
required:
- url
properties:
name:
type: string
description: Отображаемое имя профиля (опционально).
url:
type: string
format: uri
@@ -543,10 +644,12 @@ components:
properties:
id:
$ref: "#/components/schemas/ResourceId"
name:
community:
type: string
kind:
description: Техническое значение BGP community (строка для BIRD, например 65001:120 или large JSON в value_json).
title:
type: string
description: Человекочитаемое название для UI и фильтров.
additionalProperties: true
BgpPeer:
@@ -564,6 +667,13 @@ components:
bgp_speaker_id:
type: ["string", "null"]
description: "`null` - политика для всех спикеров."
policies_json:
type: string
description: >
JSON-объект (строка). Поля `local_ipv4`, `local_ipv6`, `local_asn` переопределяют
глобальные `bird_local_ipv4` / `bird_local_ipv6` / `bird_local_asn` тенанта.
Если эффективный локальный адрес или ASN пира отличается от дефолтов тенанта (`bird_local_*`),
в блок `protocol bgp … from bgp_template` добавляется строка `local … as …`.
additionalProperties: true
BgpSpeaker:
@@ -600,7 +710,9 @@ components:
PrefixSnapshotItem:
type: object
description: Элемент материализованного снимка префиксов (детали - по реализации).
description: >
Элемент материализованного снимка. Поле prefix обычно содержит CIDR;
для модулей AS_PREFIXES допускается ключ вида as:<номер_asn> (не CIDR).
additionalProperties: true
Job:
@@ -648,8 +760,14 @@ components:
VersionInfo:
type: object
properties:
version:
type: string
description: Semver сборки (git tag без префикса v).
example: "1.2.3"
api_version:
type: string
deprecated: true
description: Alias поля `version` (сохранён для обратной совместимости).
git_sha:
type: string
build_time:
@@ -678,7 +796,30 @@ components:
Settings:
type: object
description: KV настройки tenant/глобальные лимиты и feature flags.
description: |
KV настройки tenant/глобальные лимиты и feature flags.
Параметры BIRD (строки в `global_settings`, JSON-значения — обычно строка или число):
`bird_router_id`, `bird_local_ipv4`, `bird_local_ipv6`, `bird_local_asn`;
если задан `bird_bgp_source_ipv4`, он подставляется как BIRD `router id` (перекрывает `bird_router_id`).
Ключи `bird_bgp_source_ipv4` / `bird_bgp_source_ipv6` в сгенерированном BGP для пиров не используются (оставлены для совместимости API).
Шаблон BGP в конфиге: `local as <bird_local_asn>;` без локального IP.
properties:
bird_router_id:
type: string
description: BIRD `router id` (IPv4 dotted quad), если не задан `bird_bgp_source_ipv4`.
bird_local_ipv4:
type: string
bird_local_ipv6:
type: string
bird_local_asn:
type: integer
bird_bgp_source_ipv4:
type: string
description: |
Если задан — используется как BIRD `router id` (IPv4). В блоках `protocol bgp … from bgp_template` строка `source address` не генерируется.
bird_bgp_source_ipv6:
type: string
description: Зарезервировано; в текущей генерации BGP не используется.
additionalProperties: true
RevisionDiff:
@@ -713,6 +854,8 @@ components:
DohProfilePatch:
type: object
properties:
name:
type: string
url:
type: string
format: uri
@@ -723,20 +866,20 @@ components:
BgpCommunityCreate:
type: object
required: [name]
required: [community]
properties:
name:
community:
type: string
kind:
title:
type: string
additionalProperties: true
BgpCommunityPatch:
type: object
properties:
name:
community:
type: string
kind:
title:
type: string
additionalProperties: true
@@ -832,11 +975,31 @@ paths:
default:
$ref: "#/components/responses/DefaultProblem"
/version:
get:
tags: [System]
summary: Версия сборки (корневой путь)
description: |
Аналог `GET /v1/version`. Публичный маршрут без аутентификации.
Semver в поле `version` задаётся при сборке Docker-образов из git-тега.
operationId: getVersionRoot
responses:
"200":
description: Метаданные сборки.
content:
application/json:
schema:
$ref: "#/components/schemas/VersionInfo"
default:
$ref: "#/components/responses/DefaultProblem"
/v1/version:
get:
tags: [System]
summary: Версия сборки
description: Версия API и control-plane (`git_sha`, `build_time` и др.).
description: |
Semver control-plane и метаданные сборки (`git_sha`, `build_time`).
Дублирует `GET /version`.
operationId: getVersion
responses:
"200":
@@ -852,7 +1015,7 @@ paths:
get:
tags: [Modules]
summary: Список модулей
description: Модули tenant с опциональными фильтрами по типу и флагу `enabled`.
description: Модули tenant с опциональными фильтрами по типу и флагу `enabled` (фильтры применяются сервером).
operationId: listModules
parameters:
- $ref: "#/components/parameters/TenantId"
@@ -881,6 +1044,90 @@ paths:
$ref: "#/components/responses/Unauthorized"
default:
$ref: "#/components/responses/DefaultProblem"
/v1/router-lists/catalog:
get:
tags: [Modules]
summary: Агрегированный каталог для router-lists-ui
description: |
Возвращает в одном ответе:
- модули типов `DOMAINS`, `IP_RANGES`, `AS_PREFIXES`;
- entries по каждому модулю;
- справочник community (`id`, `community`, `title`).
operationId: getRouterListsCatalog
parameters:
- $ref: "#/components/parameters/TenantId"
responses:
"200":
description: Агрегированные данные для списков UI.
content:
application/json:
schema:
type: object
required: [modules, domains, asns, ip_ranges, communities]
properties:
modules:
type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/Module"
domains:
type: object
required: [items]
properties:
items:
type: array
items:
type: object
required: [module_id, entry]
properties:
module_id:
$ref: "#/components/schemas/ResourceId"
entry:
$ref: "#/components/schemas/DomainEntry"
asns:
type: object
required: [items]
properties:
items:
type: array
items:
type: object
required: [module_id, entry]
properties:
module_id:
$ref: "#/components/schemas/ResourceId"
entry:
$ref: "#/components/schemas/AsEntry"
ip_ranges:
type: object
required: [items]
properties:
items:
type: array
items:
type: object
required: [module_id, entry]
properties:
module_id:
$ref: "#/components/schemas/ResourceId"
entry:
$ref: "#/components/schemas/IpRangeEntry"
communities:
type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/BgpCommunity"
"401":
$ref: "#/components/responses/Unauthorized"
default:
$ref: "#/components/responses/DefaultProblem"
post:
tags: [Modules]
summary: Создать модуль
@@ -1166,7 +1413,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/AsEntryCreate"
$ref: "#/components/schemas/AsEntryPatch"
responses:
"200":
description: Успешно.
@@ -1399,6 +1646,9 @@ paths:
summary: Запустить ingest
description: |
Запуск обновления данных модуля (CDN / DoH / AS в зависимости от типа).
Итоговая ревизия и фрагменты BIRD строятся **по всем включённым модулям тенанта**:
обновляемый модуль пересчитывается заново, остальные — по текущему состоянию в БД/источниках,
так что префиксы ASN, CDN, IP и т.д. суммируются в одном конфиге.
Для `IP_RANGES` данные обычно только в БД: сервер может вернуть **204** (no-op) или **400**, если refresh не поддерживается - поведение фиксируется в реализации.
Рекомендуется передавать `Idempotency-Key`.
operationId: postModuleRefresh
@@ -1431,6 +1681,74 @@ paths:
default:
$ref: "#/components/responses/DefaultProblem"
/v1/modules/{module_id}/entries.csv:
parameters:
- $ref: "#/components/parameters/TenantId"
- $ref: "#/components/parameters/ModuleId"
get:
tags: [Modules]
summary: Экспорт записей модуля в CSV
description: |
Доступно для типов модулей `AS_PREFIXES`, `DOMAINS`, `IP_RANGES`.
Возвращает CSV с колонками:
- AS: `asn,community`
- Домены: `domain,community`
- IP ranges: `ipRange,community`
operationId: exportModuleEntriesCsv
responses:
"200":
description: CSV-файл записей модуля.
content:
text/csv:
schema:
type: string
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/UnprocessableEntity"
default:
$ref: "#/components/responses/DefaultProblem"
post:
tags: [Modules]
summary: Импорт записей модуля из CSV
description: |
Импортирует CSV в модуль типов `AS_PREFIXES`, `DOMAINS`, `IP_RANGES`.
Поддерживаемые заголовки:
- `asn,community`
- `domain,community`
- `ipRange,community`
В поле `community` можно передавать либо ID community, либо её значение.
operationId: importModuleEntriesCsv
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
text/csv:
schema:
type: string
responses:
"200":
description: Импорт завершён.
content:
application/json:
schema:
type: object
required: [imported, module_type]
properties:
imported:
type: integer
minimum: 0
module_type:
type: string
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/UnprocessableEntity"
default:
$ref: "#/components/responses/DefaultProblem"
/v1/doh-profiles:
get:
tags: [DoH profiles]
@@ -1485,6 +1803,40 @@ paths:
default:
$ref: "#/components/responses/DefaultProblem"
/v1/modules/{module_id}/cdn-sources/preview:
parameters:
- $ref: "#/components/parameters/TenantId"
- $ref: "#/components/parameters/ModuleId"
post:
tags: [Modules]
summary: Предпросмотр префиксов из CDN-источника
description: >
Загружает URL, парсит как plaintext или json и возвращает список извлечённых префиксов (до 100 записей).
operationId: previewCdnSource
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CdnPreviewRequest"
responses:
"200":
description: Успешно.
content:
application/json:
schema:
$ref: "#/components/schemas/CdnPreviewResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/UnprocessableEntity"
default:
$ref: "#/components/responses/DefaultProblem"
/v1/doh-profiles/{id}:
parameters:
- $ref: "#/components/parameters/TenantId"
@@ -1687,6 +2039,9 @@ paths:
post:
tags: [Peers]
summary: Создать пира
description: >
Создаёт BGP-пира и инициирует быстрый reconcile пиров (job `peer_reconcile`) без module ingest.
После reconcile автоматически запускается apply на спикеры.
operationId: createPeer
parameters:
- $ref: "#/components/parameters/TenantId"
@@ -1729,7 +2084,11 @@ paths:
patch:
tags: [Peers]
summary: Обновить пира
description: Политики, neighbor, ASN, привязка к `bgp_speaker_id` или `null` для всех спикеров.
description: >
Политики (`policies_json`: `local_ipv4`, `local_ipv6`, `local_asn`), neighbor, ASN,
привязка к `bgp_speaker_id` или `null` для всех спикеров.
Изменение инициирует быстрый reconcile пиров (job `peer_reconcile`) без module ingest
и затем авто-apply на спикеры.
operationId: patchPeer
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
@@ -1753,6 +2112,9 @@ paths:
delete:
tags: [Peers]
summary: Удалить или отключить пира
description: >
Удаление/отключение инициирует быстрый reconcile пиров (job `peer_reconcile`) без module ingest
и затем авто-apply на спикеры.
operationId: deletePeer
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
+1 -1
View File
@@ -8,7 +8,7 @@ EvoBGP — это control plane для описания источников п
| Тип | Назначение |
|-----|------------|
| `AS_PREFIXES` | ASN и связанные префиксы |
| `AS_PREFIXES` | Номера AS и привязка к BGP community (без статического CIDR в записи) |
| `CDN_CIDRS` | CIDR из внешних CDN-источников (URL, виды источников) |
| `DOMAINS` | FQDN с привязкой к BGP community; опционально DoH-профили |
| `IP_RANGES` | Статические CIDR + `community_id` (данные в БД, без внешнего ingest по URL) |
+44 -7
View File
@@ -109,35 +109,72 @@ docker compose --profile microvps down -v
Из каталога `deploy\compose` (как в варианте 1: `.env` из `.env.example`, при необходимости `docker login git.shts.su`):
1. Подготовьте отдельный файл параметров защищенного UI:
```powershell
Copy-Item .env.web-sec.example .env.web-sec -Force
```
2. Заполните в `.env.web-sec`:
- `WEBUI_DOMAIN` — домен панели;
- `WEBUI_IP_WHITELIST` — список разрешенных IP/CIDR через запятую;
- `LETSENCRYPT_EMAIL` — email для ACME;
- `CF_DNS_API_TOKEN` — Cloudflare token для `DNS challenge` (минимальные права `Zone:DNS:Edit` на нужной зоне).
3. В Cloudflare для `WEBUI_DOMAIN` используйте запись в режиме **DNS only** (серый облачок), указывающую на публичный IP VPS.
4. Запускайте compose с двумя env-файлами:
```powershell
# С ужатыми лимитами и EVOBGP_BROKER_URL=nats://… (ориентир под ~1 ГиБ RAM на хосте)
docker compose -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full pull
docker compose -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full up -d
docker compose --env-file .env --env-file .env.web-sec -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full pull
docker compose --env-file .env --env-file .env.web-sec -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full up -d
```
Только профиль `microvps-full` и **без** файла `docker-compose.microvps-full.yaml` (лимиты как в базовом compose, без принудительной подстановки брокера в `evobgp-all`):
```powershell
docker compose --profile microvps-full pull
docker compose --profile microvps-full up -d
docker compose --env-file .env --env-file .env.web-sec --profile microvps-full pull
docker compose --env-file .env --env-file .env.web-sec --profile microvps-full up -d
```
При необходимости задайте брокер вручную для `evobgp-all` (override или `.env` рядом с compose): `EVOBGP_BROKER_URL=nats://nats:4222`.
| Порт | Назначение |
|------|------------|
| **3000** | Web UI (nginx → API под капотом) |
| **80** | HTTP вход (редирект на HTTPS через Traefik) |
| **443** | HTTPS Web UI (Traefik + Let's Encrypt) |
| **8080** | Прямой HTTP API (`evobgp-all`) |
| **9090** | Prometheus (`prometheus-microvps`) |
| **4222** | NATS |
| **179** | BGP (BIRD2), как в варианте 1 |
Проверка UI: откройте `http://<IP-вашей-VPS>:3000`. Health API: `http://<IP>:8080/v1/health`.
Проверка UI:
- `http://<WEBUI_DOMAIN>` должен редиректить на `https://<WEBUI_DOMAIN>`;
- с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS;
- с неразрешенного IP Traefik вернет `403`.
Health API: `http://<IP>:8080/v1/health`.
### Auto-updater для standalone stack (без рестарта BIRD2)
Для `stack.microvps-full.yaml` можно включить автообновление только выбранных сервисов (например, `evobgp-all,evobgp-web`) по digest образов в registry.
В файле `deploy\compose\.env.stack.microvps-full`:
- `AUTO_UPDATE_ENABLED=1` — включить updater;
- `AUTO_UPDATE_INTERVAL_SEC=300` — период проверки;
- `AUTO_UPDATE_SERVICES=evobgp-all,evobgp-web` — allowlist сервисов;
- `AUTO_UPDATE_PROTECTED_SERVICES=bird2` — список защищённых сервисов (по умолчанию `bird2`).
Updater делает `pull` и при изменении образа выполняет `up -d --no-deps` только для сервиса из allowlist. `bird2` в обновление не попадает, пока явно указан в protected-списке.
Важно: updater использует доступ к `docker.sock` (высокие привилегии), включайте осознанно.
Остановка (если поднимали с двумя `-f`):
```powershell
docker compose -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full down
docker compose --env-file .env --env-file .env.web-sec -f docker-compose.yaml -f docker-compose.microvps-full.yaml --profile microvps-full down
```
## Вариант 2: Docker, профиль reference
+72
View File
@@ -0,0 +1,72 @@
# Релизы и версионирование EvoBGP
EvoBGP использует [Conventional Commits](https://www.conventionalcommits.org/) и [semantic-release](https://semantic-release.gitbook.io/) для полностью автоматических релизов на Gitea (`git.shts.su`). Ручное повышение версии в коде не требуется.
## Как определяется версия
| Тип коммита | Bump |
|-------------|------|
| `feat` | minor (1.0.0 → 1.1.0) |
| `fix`, `perf` | patch (1.0.0 → 1.0.1) |
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
| `docs`, `chore`, `ci`, `test`, `refactor` | без релиза |
Первый релиз при отсутствии git-тегов — **1.0.0**, если есть releasable-коммиты.
Подробные правила сообщений коммитов: [.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc).
## CI-пайплайн (один push в main)
```text
push/merge в main
→ CI: openapi, web, go, bird2 (параллельно)
→ job release (в том же workflow, после quality gates):
→ semantic-release: git tag vX.Y.Z на текущий commit (без доп. commit)
→ Gitea Release + CHANGELOG.md как attachment
→ docker buildx bake с VERSION=X.Y.Z
→ образы: latest, vX.Y.Z, X.Y.Z, sha-*, короткий SHA
```
Pull request: только quality gates + commitlint; релиз и образы **не** публикуются.
Workflow: [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml) (job **release**).
Конфиг semantic-release: [.releaserc.json](../.releaserc.json) — без `@semantic-release/git` (CHANGELOG не коммитится в репозиторий).
## Секреты Gitea
Один PAT — **`ACTIONS_PAT`** (Settings → Actions → Secrets).
| Право PAT | Зачем |
|-----------|--------|
| push tags | git tag `vX.Y.Z` на commit merge |
| releases | Gitea Release + notes |
| packages (Container Registry) | push образов |
Fallback: **`gitea.token`** (нужны права на releases и packages).
## Источник правды для версии в runtime
Semver из git-тега пробрасывается в Go-бинарники через `-ldflags` при сборке Docker (`deploy/docker/gobinary/Dockerfile`). Пакет [`internal/version`](../internal/version/version.go):
- локально (`go run`) — `version: "dev"`
- в образе после релиза — совпадает с тегом (например `1.2.3`)
API: `GET /version`, `GET /v1/version` — поля `version`, `git_sha`, `build_time`.
Web UI показывает версию из API (footer sidebar, страница «Мониторинг»).
## CHANGELOG
Release notes — в Gitea Release; файл `CHANGELOG.md` генерируется в CI и прикрепляется как asset, **не** попадает в git history.
## Проверка после релиза
1. Один run workflow **CI** на push в main: job **release** зелёный.
2. Gitea: тег `vX.Y.Z` на том же commit, что и merge; Release с notes.
3. Container Registry: `evobgp-api:vX.Y.Z`, `evobgp-api:X.Y.Z`, `evobgp-api:latest`.
4. `curl http://localhost:8080/version` → `"version":"X.Y.Z"`.
## Первый релиз (bootstrap)
Merge в `main` с `feat(release): ...` → **v1.0.0** в том же CI run.
+119
View File
@@ -0,0 +1,119 @@
# Интеграция router-lists-ui с EvoBGP API
Документ описывает подключение разделов `Домены`, `IP-диапазоны`, `AS`, `Community` из проекта `router-lists-ui` напрямую к EvoBGP API (`/v1`).
Источник правды по контракту: [openapi.yaml](openapi.yaml).
## 1. Базовые требования
- Base URL API: `https://<evobgp-host>/v1`
- Аутентификация: `Authorization: Bearer <api_key>`
- CORS на стороне EvoBGP: переменная `EVOBGP_CORS_ORIGINS`
- Роли:
- чтение: `viewer+`
- изменение данных: `editor+`
- apply/деплой: `operator`
## 2. Маппинг legacy -> EvoBGP
| Раздел UI | Legacy endpoint | EvoBGP endpoint |
|---|---|---|
| Домены | `/api/domains-new` | `/v1/modules?type=DOMAINS` + `/v1/modules/{module_id}/domain-entries` |
| IP-диапазоны | `/api/ip-ranges` | `/v1/modules?type=IP_RANGES` + `/v1/modules/{module_id}/ip-range-entries` |
| AS | `/api/asns` | `/v1/modules?type=AS_PREFIXES` + `/v1/modules/{module_id}/as-entries` |
| Community | `/api/communities` | `/v1/communities` |
Для упрощённой интеграции доступен агрегированный endpoint: `GET /v1/router-lists/catalog` (модули + entries + communities в одном ответе).
## 3. Маппинг полей
| Legacy модель | EvoBGP модель | Комментарий |
|---|---|---|
| `{ domain, community }` | `DomainEntry { fqdn, community_id }` | `community` в UI резолвится в `community_id` через `/v1/communities` |
| `{ ipRange, community }` | `IpRangeEntry { prefix, community_id }` | Для `IP_RANGES` `community_id` обязателен |
| `{ domain, type }` (AS) | `AsEntry { asn, community_id }` | `domain` legacy = ASN |
| `{ value, name }` (Community dict) | `BgpCommunity { community, title }` | `value -> community`, `name -> title` |
## 4. Алгоритм работы по разделам
### 4.1 Домены / IP / AS (общая схема)
1. Найти модуль нужного типа через `GET /v1/modules?type=...`.
2. Если модуль отсутствует, создать через `POST /v1/modules`.
3. Загрузить entries:
- домены: `GET /v1/modules/{module_id}/domain-entries`
- IP: `GET /v1/modules/{module_id}/ip-range-entries`
- AS: `GET /v1/modules/{module_id}/as-entries`
4. При сохранении:
- удалить отсутствующие записи (`DELETE .../{entry_id}`)
- обновить изменённые (`PATCH .../{entry_id}`)
- добавить новые (`POST ...`)
### 4.2 Community
1. Получить список: `GET /v1/communities`
2. Для сохранения diff:
- новые -> `POST /v1/communities`
- изменённые -> `PATCH /v1/communities/{id}`
- удалённые -> `DELETE /v1/communities/{id}`
## 5. Разрешение community
- Перед работой с entries загрузить `/v1/communities`.
- Основной путь: сопоставление по `id`.
- Для UI-формы использовать значение `community` (например, `65001:120`) и перед записью преобразовывать в `community_id`.
- Если community не найдена:
- для `IP_RANGES` считать ошибкой валидации;
- для `DOMAINS` и `AS_PREFIXES` можно передать `null` только если это допускается бизнес-логикой.
## 6. Пагинация и ошибки
- Списки используют `cursor + limit`.
- Формат ошибок: `application/problem+json` (RFC 9457).
- Базовая обработка:
- `401` — неверный/отсутствующий токен
- `403` — недостаточно прав
- `404` — ресурс не найден
- `422` — ошибка валидации
## 7. Примеры (PowerShell)
```powershell
$base = "http://localhost:8080/v1"
$token = "YOUR_API_KEY"
$h = @{ Authorization = "Bearer $token" }
# 1) Найти модуль DOMAINS
$modules = Invoke-RestMethod -Uri "$base/modules?type=DOMAINS&limit=50" -Headers $h
$moduleId = $modules.items[0].id
# 2) Прочитать домены
Invoke-RestMethod -Uri "$base/modules/$moduleId/domain-entries?limit=100" -Headers $h
```
```powershell
# Создать community
$body = @{
community = "65001:120"
title = "Video"
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$base/communities" -Headers $h -ContentType "application/json" -Body $body
```
## 8. Настройки router-lists-ui для прямой интеграции
Рекомендуемые переменные frontend:
- `VITE_EVOBGP_API_URL` (по умолчанию `/v1`)
- `VITE_EVOBGP_API_TOKEN` (опционально; либо хранить токен в `localStorage` как `evobgp_api_token`)
Для dev-прокси Vite добавить маршрут `/v1 -> http://localhost:8080`.
## 9. Контрольный список интеграции
- [ ] `Домены`: list/create/update/delete работают через `/v1/modules/{id}/domain-entries`
- [ ] `IP-диапазоны`: list/create/update/delete работают через `/v1/modules/{id}/ip-range-entries`
- [ ] `AS`: list/create/update/delete работают через `/v1/modules/{id}/as-entries`
- [ ] `Community`: CRUD работает через `/v1/communities`
- [ ] ошибки `problem+json` корректно отображаются в UI
+9 -5
View File
@@ -1,10 +1,11 @@
module evobgp
go 1.22
go 1.24.0
require (
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/miekg/dns v1.1.72
github.com/prometheus/client_golang v1.20.5
modernc.org/sqlite v1.34.5
)
@@ -24,10 +25,13 @@ require (
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
+16 -12
View File
@@ -28,6 +28,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -49,19 +51,21 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+156
View File
@@ -0,0 +1,156 @@
// Package asnresolve fetches IP prefixes announced by an ASN (control-plane ingest).
package asnresolve
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"os"
"sort"
"strconv"
"strings"
"time"
)
// DefaultRIPEStatURL is the RIPEstat announced-prefixes data call (no API key).
const DefaultRIPEStatURL = "https://stat.ripe.net/data/announced-prefixes/data.json"
// DefaultASOverviewURL is the RIPEstat as-overview data call (holder / org name, no API key).
const DefaultASOverviewURL = "https://stat.ripe.net/data/as-overview/data.json"
// AnnouncedPrefixes returns currently announced IPv4/IPv6 prefixes for the ASN (best-effort via RIPEstat).
func AnnouncedPrefixes(ctx context.Context, hc *http.Client, asn int64) ([]netip.Prefix, error) {
if hc == nil {
hc = http.DefaultClient
}
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL"))
if base == "" {
base = DefaultRIPEStatURL
}
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
resp, err := hc.Do(req)
if err != nil {
return nil, fmt.Errorf("ripestat fetch AS%d: %w", asn, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ripestat AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
}
var wrap struct {
Status string `json:"status"`
Data struct {
Prefixes []struct {
Prefix string `json:"prefix"`
} `json:"prefixes"`
} `json:"data"`
Messages [][]string `json:"messages"`
}
if err := json.Unmarshal(body, &wrap); err != nil {
return nil, fmt.Errorf("ripestat AS%d: json: %w", asn, err)
}
if wrap.Status != "" && wrap.Status != "ok" {
return nil, fmt.Errorf("ripestat AS%d: status %q", asn, wrap.Status)
}
var out []netip.Prefix
for _, row := range wrap.Data.Prefixes {
p := strings.TrimSpace(row.Prefix)
if p == "" {
continue
}
pfx, err := netip.ParsePrefix(p)
if err != nil {
continue
}
out = append(out, pfx.Masked())
}
sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
return out, nil
}
// ASHolderName returns the holder / organization label for the ASN from RIPEstat as-overview (best-effort).
func ASHolderName(ctx context.Context, hc *http.Client, asn int64) (string, error) {
if hc == nil {
hc = http.DefaultClient
}
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL"))
if base == "" {
base = DefaultASOverviewURL
}
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
resp, err := hc.Do(req)
if err != nil {
return "", fmt.Errorf("ripestat as-overview AS%d: %w", asn, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("ripestat as-overview AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
}
var wrap struct {
Status string `json:"status"`
Data struct {
Holder string `json:"holder"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrap); err != nil {
return "", fmt.Errorf("ripestat as-overview AS%d: json: %w", asn, err)
}
if wrap.Status != "" && wrap.Status != "ok" {
return "", fmt.Errorf("ripestat as-overview AS%d: status %q", asn, wrap.Status)
}
return strings.TrimSpace(wrap.Data.Holder), nil
}
func truncateForErr(b []byte, n int) string {
s := string(b)
if len(s) > n {
return s[:n] + "…"
}
return s
}
// PolitePause is a short delay between upstream ASN lookups (same refresh).
func PolitePause() {
d := 150 * time.Millisecond
if s := strings.TrimSpace(os.Getenv("EVOBGP_ASN_RESOLVE_PAUSE_MS")); s != "" {
if ms, err := parsePositiveInt(s); err == nil && ms > 0 {
d = time.Duration(ms) * time.Millisecond
}
}
time.Sleep(d)
}
func parsePositiveInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil || n <= 0 {
return 0, fmt.Errorf("invalid")
}
return n, nil
}
+54
View File
@@ -0,0 +1,54 @@
package asnresolve
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestAnnouncedPrefixes_Mock(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("resource") != "AS64496" {
t.Fatalf("resource: %q", r.URL.Query().Get("resource"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok","data":{"prefixes":[{"prefix":"203.0.113.0/24"},{"prefix":"2001:db8::/32"}]}}`))
}))
defer ts.Close()
t.Setenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL", ts.URL)
t.Setenv("EVOBGP_ASN_RESOLVE_PAUSE_MS", "1")
pfx, err := AnnouncedPrefixes(context.Background(), ts.Client(), 64496)
if err != nil {
t.Fatal(err)
}
if len(pfx) != 2 {
t.Fatalf("got %d prefixes", len(pfx))
}
if pfx[0].String() != "2001:db8::/32" || pfx[1].String() != "203.0.113.0/24" {
t.Fatalf("order or values: %#v", pfx)
}
}
func TestASHolderName_Mock(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("resource") != "AS64496" {
t.Fatalf("resource: %q", r.URL.Query().Get("resource"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok","data":{"holder":"Example Networks Ltd"}}`))
}))
defer ts.Close()
t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", ts.URL)
name, err := ASHolderName(context.Background(), ts.Client(), 64496)
if err != nil {
t.Fatal(err)
}
if name != "Example Networks Ltd" {
t.Fatalf("holder: %q", name)
}
}
+11
View File
@@ -43,6 +43,9 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
// Phase 1: write staging tree
for rel, content := range rev.PreviewFragments {
rel = strings.TrimPrefix(rel, "/")
if !isDeployableBirdFragment(rel) {
continue
}
dst := filepath.Join(staging, rel)
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
@@ -70,6 +73,9 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
// Phase 2: atomic swap into active
for rel, content := range rev.PreviewFragments {
rel = strings.TrimPrefix(rel, "/")
if !isDeployableBirdFragment(rel) {
continue
}
dst := filepath.Join(active, rel)
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
_ = restoreLKG(lkg, active)
@@ -93,6 +99,11 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
return nil
}
// isDeployableBirdFragment skips UI-only preview keys (e.g. flattened full config) not referenced from bird.conf.
func isDeployableBirdFragment(rel string) bool {
return rel != "" && !strings.HasPrefix(rel, "_")
}
func snapshotBirdTree(activeRoot, dstRoot string) error {
if err := os.MkdirAll(dstRoot, 0o755); err != nil {
return err
+102
View File
@@ -5,6 +5,108 @@ import (
"strings"
)
// BGP template names referenced by generated peers (BIRD 2).
const (
BGPTemplateNameV4 = "bgp_template"
BGPTemplateNameV6 = "bgp_template_v6"
)
// BGPTemplatesOptions holds ASN and export filters for template bgp bgp_template (+ v6 mirror).
// Templates use "local as <asn>;" only (no local IP); peers add neighbor / multihop / passive and optional "local … as …" override.
type BGPTemplatesOptions struct {
LocalASN uint32
ExportFilterV4 string
ExportFilterV6 string
}
// RenderBGPTemplates renders two template bgp blocks (IPv4 and IPv6 AFI).
func RenderBGPTemplates(opts BGPTemplatesOptions) (string, error) {
if opts.LocalASN == 0 {
return "", fmt.Errorf("birdfmt: template local ASN must be non-zero")
}
exp4 := "all"
if strings.TrimSpace(opts.ExportFilterV4) != "" {
exp4 = "filter " + strings.TrimSpace(opts.ExportFilterV4)
}
exp6 := "all"
if strings.TrimSpace(opts.ExportFilterV6) != "" {
exp6 = "filter " + strings.TrimSpace(opts.ExportFilterV6)
}
var b strings.Builder
fmt.Fprintf(&b, "template bgp %s {\n", BGPTemplateNameV4)
fmt.Fprintf(&b, " local as %d;\n", opts.LocalASN)
b.WriteString(" ipv4 {\n")
b.WriteString(" import none;\n")
b.WriteString(" export ")
b.WriteString(exp4)
b.WriteString(";\n")
b.WriteString(" };\n")
b.WriteString(" hold time 90;\n")
b.WriteString(" keepalive time 30;\n")
b.WriteString("}\n\n")
fmt.Fprintf(&b, "template bgp %s {\n", BGPTemplateNameV6)
fmt.Fprintf(&b, " local as %d;\n", opts.LocalASN)
b.WriteString(" ipv6 {\n")
b.WriteString(" import none;\n")
b.WriteString(" export ")
b.WriteString(exp6)
b.WriteString(";\n")
b.WriteString(" };\n")
b.WriteString(" hold time 90;\n")
b.WriteString(" keepalive time 30;\n")
b.WriteString("}\n")
return b.String(), nil
}
// BGPPeerFromTemplateOptions describes protocol bgp NAME from TEMPLATE { … }.
type BGPPeerFromTemplateOptions struct {
ProtocolName string
TemplateName string
NeighborIP string
NeighborASN uint32
// If set, emits "local … as …" before neighbor (overrides template local/ASN for this peer).
OverrideLocalIP string
OverrideLocalASN uint32
}
// RenderProtocolBGPFromTemplate renders protocol bgp … from TEMPLATE { neighbor; multihop; passive; }.
func RenderProtocolBGPFromTemplate(opts BGPPeerFromTemplateOptions) (string, error) {
if strings.TrimSpace(opts.ProtocolName) == "" {
return "", fmt.Errorf("birdfmt: protocol name is required")
}
if strings.TrimSpace(opts.TemplateName) == "" {
return "", fmt.Errorf("birdfmt: template name is required")
}
if strings.TrimSpace(opts.NeighborIP) == "" {
return "", fmt.Errorf("birdfmt: neighbor is required")
}
if opts.NeighborASN == 0 {
return "", fmt.Errorf("birdfmt: neighbor ASN must be non-zero")
}
if opts.OverrideLocalIP != "" && opts.OverrideLocalASN == 0 {
return "", fmt.Errorf("birdfmt: override local ASN is required when override local IP is set")
}
var b strings.Builder
b.WriteString("protocol bgp ")
b.WriteString(strings.TrimSpace(opts.ProtocolName))
b.WriteString(" from ")
b.WriteString(strings.TrimSpace(opts.TemplateName))
b.WriteString(" {\n")
if ip := strings.TrimSpace(opts.OverrideLocalIP); ip != "" {
b.WriteString(" local ")
b.WriteString(ip)
fmt.Fprintf(&b, " as %d;\n", opts.OverrideLocalASN)
}
b.WriteString(" neighbor ")
b.WriteString(strings.TrimSpace(opts.NeighborIP))
fmt.Fprintf(&b, " as %d;\n", opts.NeighborASN)
b.WriteString(" multihop;\n")
b.WriteString(" passive;\n")
b.WriteString("}\n")
return b.String(), nil
}
// BGPPeerIPv4Options describes a single BGP session (BIRD 2, IPv4 AF).
type BGPPeerIPv4Options struct {
ProtocolName string // e.g. evobgp_peer_uplink
+45
View File
@@ -27,3 +27,48 @@ func TestRenderProtocolBGPIPv4_ExportAll(t *testing.T) {
t.Fatal(got)
}
}
func TestRenderBGPTemplates_Validation(t *testing.T) {
_, err := RenderBGPTemplates(BGPTemplatesOptions{})
if err == nil {
t.Fatal("expected error")
}
}
func TestRenderProtocolBGPFromTemplate_MultihopPassive(t *testing.T) {
got, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "evobgp_p_x",
TemplateName: BGPTemplateNameV4,
NeighborIP: "94.142.140.141",
NeighborASN: 65002,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "from bgp_template") {
t.Fatal(got)
}
if !strings.Contains(got, "multihop;") || !strings.Contains(got, "passive;") {
t.Fatal(got)
}
if strings.Contains(got, "source address") || strings.Contains(got, "strict bind") {
t.Fatal(got)
}
}
func TestRenderProtocolBGPFromTemplate_OverrideLocal(t *testing.T) {
got, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "p",
TemplateName: BGPTemplateNameV4,
NeighborIP: "192.0.2.2",
NeighborASN: 2,
OverrideLocalIP: "10.0.0.1",
OverrideLocalASN: 65099,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "local 10.0.0.1 as 65099;") {
t.Fatal(got)
}
}
+175
View File
@@ -0,0 +1,175 @@
package birdfmt
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
)
var standardPairRE = regexp.MustCompile(`^\s*(\d{1,5}):(\d{1,5})\s*$`)
// badCommunityAddRE matches invalid BIRD filter calls like bgp_community.add(65000,1)
// (first argument is an int). Valid form is bgp_community.add((65000,1)); — note the pair tuple.
var badCommunityAddRE = regexp.MustCompile(`\b(?:bgp_community|bgp_large_community)\.add\(\s*\d`)
// RouteCommunityAttrs returns BIRD 2 lines (indented with 4 spaces) for use inside a static `route … { … }` block.
// Empty string means no community attributes.
func RouteCommunityAttrs(kind, name, valueJSON string) (string, error) {
name = strings.TrimSpace(name)
raw := strings.TrimSpace(valueJSON)
if raw == "" {
raw = "{}"
}
if s, ok := parseLargeFromJSONLoose(raw); ok {
if err := validateCommunityRouteBody(s); err != nil {
return "", err
}
return s, nil
}
_ = kind // reserved for future kinds (e.g. extended communities)
if m := standardPairRE.FindStringSubmatch(name); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) == nil {
if v, ok := obj["standard"].(string); ok {
if m := standardPairRE.FindStringSubmatch(v); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
if arr, ok := obj["standard"].([]any); ok {
if len(arr) == 1 {
return "", fmt.Errorf(`birdfmt: value_json "standard" must be a pair [a,b] or "a:b" string, got single element`)
}
if len(arr) == 2 {
a, aok := numberToUint32String(arr[0])
b, bok := numberToUint32String(arr[1])
if aok && bok {
s, err := standardAddLine(a, b)
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
}
}
var strVal string
if json.Unmarshal([]byte(raw), &strVal) == nil {
if m := standardPairRE.FindStringSubmatch(strVal); len(m) == 3 {
s, err := standardAddLine(m[1], m[2])
if err != nil {
return "", err
}
return s, validateCommunityRouteBody(s)
}
}
return "", nil
}
// validateCommunityRouteBody rejects lines that would make BIRD 2 fail parse-check with
// "Argument 1 of 'add' expected clist / quad / ip / pair, got int" (wrong add() shape).
func validateCommunityRouteBody(body string) error {
body = strings.TrimSpace(body)
if body == "" {
return nil
}
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if badCommunityAddRE.MatchString(line) {
return fmt.Errorf("birdfmt: invalid community line (expected pair/triple tuple inside add(), e.g. bgp_community.add((65000,1));): %q", line)
}
}
return nil
}
func standardAddLine(a, b string) (string, error) {
ai, err1 := strconv.ParseUint(strings.TrimSpace(a), 10, 16)
bi, err2 := strconv.ParseUint(strings.TrimSpace(b), 10, 16)
if err1 != nil || err2 != nil {
return "", fmt.Errorf("birdfmt: standard community parts must be 0..65535 (%s:%s)", a, b)
}
return fmt.Sprintf(" bgp_community.add((%d,%d));", ai, bi), nil
}
func parseLargeFromJSONLoose(raw string) (string, bool) {
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) != nil {
return "", false
}
arr, ok := obj["large"].([]any)
if !ok || len(arr) == 0 {
return "", false
}
var b strings.Builder
for _, row := range arr {
triple, ok := row.([]any)
if !ok || len(triple) != 3 {
continue
}
a, ok1 := numberToUint32String(triple[0])
bb, ok2 := numberToUint32String(triple[1])
c, ok3 := numberToUint32String(triple[2])
if !ok1 || !ok2 || !ok3 {
continue
}
ai, _ := strconv.ParseUint(a, 10, 32)
bi, _ := strconv.ParseUint(bb, 10, 32)
ci, _ := strconv.ParseUint(c, 10, 32)
fmt.Fprintf(&b, " bgp_large_community.add((%d,%d,%d));\n", ai, bi, ci)
}
s := strings.TrimRight(b.String(), "\r\n")
return s, s != ""
}
func numberToUint32String(v any) (string, bool) {
switch x := v.(type) {
case float64:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case json.Number:
n, err := x.Int64()
if err != nil || n < 0 || n > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(n), 10), true
case int:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case int64:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case string:
n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 32)
if err != nil {
return "", false
}
return strconv.FormatUint(n, 10), true
default:
return "", false
}
}
+30
View File
@@ -0,0 +1,30 @@
package birdfmt
import "testing"
func TestRouteCommunityAttrs_StandardName(t *testing.T) {
s, err := RouteCommunityAttrs("", "65000:120", "{}")
if err != nil {
t.Fatal(err)
}
if s != " bgp_community.add((65000,120));" {
t.Fatalf("got %q", s)
}
}
func TestRouteCommunityAttrs_LargeJSON(t *testing.T) {
s, err := RouteCommunityAttrs("large", "x", `{"large":[[65000,1,2]]}`)
if err != nil {
t.Fatal(err)
}
if s != " bgp_large_community.add((65000,1,2));" {
t.Fatalf("got %q", s)
}
}
func TestRouteCommunityAttrs_StandardArrayOneElement(t *testing.T) {
_, err := RouteCommunityAttrs("", "x", `{"standard":[65000]}`)
if err == nil {
t.Fatal("expected error for single-element standard pair")
}
}
+6 -3
View File
@@ -1,13 +1,16 @@
// Engineering rules for BIRD/BGP/IP: .cursor/rules/networking-bird.mdc
//
// Package birdfmt builds BIRD 2 configuration text: main bird.conf skeleton, include
// fragments under bird.d/, export filters, static route protocols, and minimal BGP peer
// blocks. Runtime helpers parse-check configs (bird -p) and reload (birdc configure).
// fragments under bird.d/, export filters, static route protocols, BGP templates, and
// BGP peer protocols (from template or standalone). Runtime helpers parse-check configs
// (bird -p) and reload (birdc configure).
//
// # Layout
//
// Operator keeps a stable bird.conf (or generated skeleton) next to EvoBGP fragments:
//
// bird.conf — router id, protocol device, protocol direct, include lines
// bird.d/evobgp_*.conf — generated prefixes, filters, peers (names from constants)
// bird.d/evobgp_*.conf — generated prefixes, filters, BGP templates, peers (names from constants)
//
// Include order should list filter definitions before protocols that reference them
// (see StandardIncludeFragments).
+43 -10
View File
@@ -7,9 +7,27 @@ import (
"strings"
)
const maxBGPASN = 4294967295
func filterUniqueASNs(pathASNs []int64) []int64 {
seen := make(map[int64]struct{})
for _, a := range pathASNs {
if a >= 1 && a <= maxBGPASN {
seen[a] = struct{}{}
}
}
out := make([]int64, 0, len(seen))
for a := range seen {
out = append(out, a)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
// RenderExportFilterIPv4 renders a BIRD 2 filter that accepts IPv4 routes whose prefix
// is in prefixes (exact CIDR match via set membership), and rejects others.
func RenderExportFilterIPv4(filterName string, prefixes []netip.Prefix) (string, error) {
// is in prefixes (exact CIDR match via set membership), and/or routes whose AS_PATH
// contains any of pathASNs (BIRD pattern [= * ASN =]), then rejects others.
func RenderExportFilterIPv4(filterName string, prefixes []netip.Prefix, pathASNs []int64) (string, error) {
if strings.TrimSpace(filterName) == "" {
return "", fmt.Errorf("birdfmt: filter name is required")
}
@@ -27,24 +45,32 @@ func RenderExportFilterIPv4(filterName string, prefixes []netip.Prefix) (string,
}
sort.Strings(keys)
asns := filterUniqueASNs(pathASNs)
var b strings.Builder
b.WriteString("filter ")
b.WriteString(strings.TrimSpace(filterName))
b.WriteString(" {\n")
if len(keys) == 0 {
b.WriteString(" reject;\n")
} else {
if len(keys) > 0 {
b.WriteString(" if net ~ [ ")
b.WriteString(strings.Join(keys, ", "))
b.WriteString(" ] then accept;\n")
}
for _, asn := range asns {
fmt.Fprintf(&b, " if bgp_path ~ [= * %d =] then accept;\n", asn)
}
if len(keys) == 0 && len(asns) == 0 {
b.WriteString(" reject;\n")
} else {
b.WriteString(" reject;\n")
}
b.WriteString("}\n")
return b.String(), nil
}
// RenderExportFilterIPv6 renders a BIRD 2 filter for IPv6 prefixes (CIDR set, then reject).
func RenderExportFilterIPv6(filterName string, prefixes []netip.Prefix) (string, error) {
// RenderExportFilterIPv6 renders a BIRD 2 filter for IPv6 prefixes (CIDR set) and/or
// AS_PATH matches, then reject.
func RenderExportFilterIPv6(filterName string, prefixes []netip.Prefix, pathASNs []int64) (string, error) {
if strings.TrimSpace(filterName) == "" {
return "", fmt.Errorf("birdfmt: filter name is required")
}
@@ -62,16 +88,23 @@ func RenderExportFilterIPv6(filterName string, prefixes []netip.Prefix) (string,
}
sort.Strings(keys)
asns := filterUniqueASNs(pathASNs)
var b strings.Builder
b.WriteString("filter ")
b.WriteString(strings.TrimSpace(filterName))
b.WriteString(" {\n")
if len(keys) == 0 {
b.WriteString(" reject;\n")
} else {
if len(keys) > 0 {
b.WriteString(" if net ~ [ ")
b.WriteString(strings.Join(keys, ", "))
b.WriteString(" ] then accept;\n")
}
for _, asn := range asns {
fmt.Fprintf(&b, " if bgp_path ~ [= * %d =] then accept;\n", asn)
}
if len(keys) == 0 && len(asns) == 0 {
b.WriteString(" reject;\n")
} else {
b.WriteString(" reject;\n")
}
b.WriteString("}\n")
+13 -3
View File
@@ -7,7 +7,7 @@ import (
)
func TestRenderExportFilterIPv4_Empty(t *testing.T) {
got, err := RenderExportFilterIPv4("evobgp_x", nil)
got, err := RenderExportFilterIPv4("evobgp_x", nil, nil)
if err != nil {
t.Fatal(err)
}
@@ -21,7 +21,7 @@ func TestRenderExportFilterIPv4_Empty(t *testing.T) {
func TestRenderExportFilterIPv4_SkipsNonV4(t *testing.T) {
v6 := netip.MustParsePrefix("2001:db8::/32")
got, err := RenderExportFilterIPv4("f", []netip.Prefix{v6})
got, err := RenderExportFilterIPv4("f", []netip.Prefix{v6}, nil)
if err != nil {
t.Fatal(err)
}
@@ -32,7 +32,7 @@ func TestRenderExportFilterIPv4_SkipsNonV4(t *testing.T) {
func TestRenderExportFilterIPv6(t *testing.T) {
p := netip.MustParsePrefix("2001:db8::/32")
got, err := RenderExportFilterIPv6("evobgp_export_v6", []netip.Prefix{p, p})
got, err := RenderExportFilterIPv6("evobgp_export_v6", []netip.Prefix{p, p}, nil)
if err != nil {
t.Fatal(err)
}
@@ -45,3 +45,13 @@ func TestRenderExportFilterIPv6(t *testing.T) {
t.Fatalf("mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}
func TestRenderExportFilterIPv4_ASPathOnly(t *testing.T) {
got, err := RenderExportFilterIPv4("f", nil, []int64{65001, 65002})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "bgp_path ~ [= * 65001 =]") || !strings.Contains(got, "bgp_path ~ [= * 65002 =]") {
t.Fatal(got)
}
}
+8 -6
View File
@@ -9,11 +9,12 @@ import (
const (
DirBirdD = "bird.d"
FragmentPrefixesV4 = "evobgp_prefixes_v4.conf"
FragmentPrefixesV6 = "evobgp_prefixes_v6.conf"
FragmentFiltersV4 = "evobgp_filters_v4.conf"
FragmentFiltersV6 = "evobgp_filters_v6.conf"
FragmentPeers = "evobgp_peers.conf"
FragmentPrefixesV4 = "evobgp_prefixes_v4.conf"
FragmentPrefixesV6 = "evobgp_prefixes_v6.conf"
FragmentFiltersV4 = "evobgp_filters_v4.conf"
FragmentFiltersV6 = "evobgp_filters_v6.conf"
FragmentBGPTemplate = "evobgp_bgp_template.conf"
FragmentPeers = "evobgp_peers.conf"
)
// FragmentIncludePath returns a POSIX include path such as bird.d/evobgp_prefixes_v4.conf.
@@ -24,11 +25,12 @@ func FragmentIncludePath(fragmentBaseName string) string {
return DirBirdD + "/" + fragmentBaseName
}
// StandardIncludeFragments is the recommended order: filters before peers that reference them.
// StandardIncludeFragments is the recommended order: filters before BGP templates and peers.
func StandardIncludeFragments() []string {
return []string{
FragmentIncludePath(FragmentFiltersV4),
FragmentIncludePath(FragmentFiltersV6),
FragmentIncludePath(FragmentBGPTemplate),
FragmentIncludePath(FragmentPrefixesV4),
FragmentIncludePath(FragmentPrefixesV6),
FragmentIncludePath(FragmentPeers),
+55
View File
@@ -0,0 +1,55 @@
package birdfmt
import (
"context"
"os"
"strings"
)
// LocalBirdStatus is returned by GET /v1/bird/status (same host as birdc when socket is configured).
type LocalBirdStatus struct {
BirdcConfigured bool `json:"birdc_configured"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ProtocolsExcerpt string `json:"protocols_excerpt,omitempty"`
BGPSessionsTotal int `json:"bgp_sessions_total"`
BGPEstablished int `json:"bgp_established"`
// Healthy: null if birdc not configured; false if birdc failed or BGP sessions exist but none Established; true otherwise.
Healthy *bool `json:"healthy"`
}
// InspectLocalBird runs `birdc show protocols all` using EVOBGP_BIRDC_SOCKET / EVOBGP_BIRDC_BIN.
func InspectLocalBird(ctx context.Context) LocalBirdStatus {
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
if sock == "" {
return LocalBirdStatus{
BirdcConfigured: false,
Message: "EVOBGP_BIRDC_SOCKET не задан на этом процессе — статус BIRD недоступен (типично, если birdc только на ноде со спикером).",
Healthy: nil,
}
}
bin := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))
out, err := ShowProtocols(ctx, sock, bin)
if err != nil {
f := false
return LocalBirdStatus{
BirdcConfigured: true,
Error: err.Error(),
Healthy: &f,
}
}
sum := SummarizeProtocolsOutput(out)
excerpt := out
const maxExcerpt = 20000
if len(excerpt) > maxExcerpt {
excerpt = excerpt[:maxExcerpt] + "\n# … truncated …\n"
}
ok := sum.BGPSessionsTotal == 0 || sum.BGPEstablished > 0
return LocalBirdStatus{
BirdcConfigured: true,
ProtocolsExcerpt: excerpt,
BGPSessionsTotal: sum.BGPSessionsTotal,
BGPEstablished: sum.BGPEstablished,
Healthy: &ok,
}
}
+52
View File
@@ -0,0 +1,52 @@
package birdfmt
import (
"strings"
)
// ProtocolsSummary is a lightweight parse of `birdc show protocols all` (BIRD 2).
type ProtocolsSummary struct {
BGPSessionsTotal int
BGPEstablished int
RawLineCount int
}
// isBGPProtocolSummaryRow is true for BIRD "show protocols" summary rows where the
// second column (Proto) is BGP. Substring checks are unsafe: names like evobgp_* contain "bgp".
func isBGPProtocolSummaryRow(line string) bool {
line = strings.TrimSpace(line)
if line == "" {
return false
}
low := strings.ToLower(line)
if strings.HasPrefix(low, "name") || strings.HasPrefix(low, "table") {
return false
}
fields := strings.Fields(line)
if len(fields) < 2 {
return false
}
return strings.EqualFold(fields[1], "BGP")
}
// SummarizeProtocolsOutput extracts BGP session heuristics from birdc output.
func SummarizeProtocolsOutput(output string) ProtocolsSummary {
var s ProtocolsSummary
lines := strings.Split(output, "\n")
s.RawLineCount = len(lines)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
low := strings.ToLower(line)
if !isBGPProtocolSummaryRow(line) {
continue
}
s.BGPSessionsTotal++
if strings.Contains(low, "established") {
s.BGPEstablished++
}
}
return s
}
+25
View File
@@ -0,0 +1,25 @@
package birdfmt
import "testing"
func TestSummarizeProtocolsOutput(t *testing.T) {
sample := `name proto table state since info
device1 Device --- up 10:00:00
uplink BGP --- start 10:00:01 Established
`
s := SummarizeProtocolsOutput(sample)
if s.BGPSessionsTotal != 1 || s.BGPEstablished != 1 {
t.Fatalf("got %+v", s)
}
}
func TestSummarizeProtocolsOutput_evoBGPNameNotCountedAsBGP(t *testing.T) {
sample := `Name Proto Table State Since Info
evobgp_prefixes_v4 Static master4 up 17:32:14.631
evobgp_prefixes_v6 Static master6 up 17:32:14.631
`
s := SummarizeProtocolsOutput(sample)
if s.BGPSessionsTotal != 0 || s.BGPEstablished != 0 {
t.Fatalf("evobgp_* static names must not match substring bgp: got %+v", s)
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ func CountEstablishedBGPSessions(showProtocolsOutput string) int {
if line == "" || strings.HasPrefix(line, "name") || strings.HasPrefix(strings.ToLower(line), "table") {
continue
}
if !strings.Contains(strings.ToLower(line), "bgp") {
if !isBGPProtocolSummaryRow(line) {
continue
}
if strings.Contains(strings.ToLower(line), "established") {
+14 -6
View File
@@ -11,13 +11,13 @@ import (
func TestStandardLayout_GeneratorMatchesFixtures(t *testing.T) {
p4 := netip.MustParsePrefix("203.0.113.0/24")
f4, err := RenderExportFilterIPv4("evobgp_export_v4", []netip.Prefix{p4})
f4, err := RenderExportFilterIPv4("evobgp_export_v4", []netip.Prefix{p4}, nil)
if err != nil {
t.Fatal(err)
}
assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_filters_v4.conf", f4)
f6, err := RenderExportFilterIPv6("evobgp_export_v6", nil)
f6, err := RenderExportFilterIPv6("evobgp_export_v6", nil, nil)
if err != nil {
t.Fatal(err)
}
@@ -29,13 +29,21 @@ func TestStandardLayout_GeneratorMatchesFixtures(t *testing.T) {
staticV6 := RenderStaticIPv6Protocol("evobgp_prefixes_v6", nil)
assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_prefixes_v6.conf", staticV6)
peer, err := RenderProtocolBGPIPv4(BGPPeerIPv4Options{
tpl, err := RenderBGPTemplates(BGPTemplatesOptions{
LocalASN: 65001,
ExportFilterV4: "evobgp_export_v4",
ExportFilterV6: "evobgp_export_v6",
})
if err != nil {
t.Fatal(err)
}
assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_bgp_template.conf", tpl)
peer, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "evobgp_peer_ci",
LocalIP: "192.0.2.1",
LocalASN: 65001,
TemplateName: BGPTemplateNameV4,
NeighborIP: "192.0.2.2",
NeighborASN: 65002,
ExportFilter: "evobgp_export_v4",
})
if err != nil {
t.Fatal(err)
+137
View File
@@ -0,0 +1,137 @@
package birdfmt
import (
"net/netip"
"sort"
"strings"
)
// StaticRoute is one static route line in BIRD (optionally with a per-route body).
type StaticRoute struct {
Prefix netip.Prefix
RouteBody string // optional lines inside `route P unreachable { ... }`
}
// RenderStaticIPv4Routes renders a BIRD 2 protocol static block for IPv4.
func RenderStaticIPv4Routes(protocolName string, routes []StaticRoute) string {
if protocolName == "" {
protocolName = "evobgp_prefixes_v4"
}
type key struct {
p string
b string
}
uniq := make(map[key]StaticRoute)
for _, r := range routes {
if !r.Prefix.Addr().Is4() {
continue
}
p := r.Prefix.Masked()
body := strings.TrimSpace(r.RouteBody)
uniq[key{p.String(), body}] = StaticRoute{Prefix: p, RouteBody: body}
}
keys := make([]key, 0, len(uniq))
for k := range uniq {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].p != keys[j].p {
return keys[i].p < keys[j].p
}
return keys[i].b < keys[j].b
})
var b strings.Builder
b.WriteString("protocol static ")
b.WriteString(protocolName)
b.WriteString(" {\n ipv4;\n")
for _, k := range keys {
r := uniq[k]
p := r.Prefix.String()
if r.RouteBody == "" {
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable;\n")
continue
}
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable {\n")
for _, line := range strings.Split(r.RouteBody, "\n") {
line = strings.TrimRight(line, "\r")
if strings.TrimSpace(line) == "" {
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
b.WriteString(" ")
}
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteString(" };\n")
}
b.WriteString("}\n")
return b.String()
}
// RenderStaticIPv6Routes renders a BIRD 2 protocol static block for IPv6.
func RenderStaticIPv6Routes(protocolName string, routes []StaticRoute) string {
if protocolName == "" {
protocolName = "evobgp_prefixes_v6"
}
type key struct {
p string
b string
}
uniq := make(map[key]StaticRoute)
for _, r := range routes {
if !r.Prefix.Addr().Is6() {
continue
}
p := r.Prefix.Masked()
body := strings.TrimSpace(r.RouteBody)
uniq[key{p.String(), body}] = StaticRoute{Prefix: p, RouteBody: body}
}
keys := make([]key, 0, len(uniq))
for k := range uniq {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].p != keys[j].p {
return keys[i].p < keys[j].p
}
return keys[i].b < keys[j].b
})
var b strings.Builder
b.WriteString("protocol static ")
b.WriteString(protocolName)
b.WriteString(" {\n ipv6;\n")
for _, k := range keys {
r := uniq[k]
p := r.Prefix.String()
if r.RouteBody == "" {
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable;\n")
continue
}
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable {\n")
for _, line := range strings.Split(r.RouteBody, "\n") {
line = strings.TrimRight(line, "\r")
if strings.TrimSpace(line) == "" {
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
b.WriteString(" ")
}
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteString(" };\n")
}
b.WriteString("}\n")
return b.String()
}
+20
View File
@@ -0,0 +1,20 @@
package birdfmt
import (
"net/netip"
"strings"
"testing"
)
func TestRenderStaticIPv4Routes_WithCommunity(t *testing.T) {
p := netip.MustParsePrefix("203.0.113.0/24")
s := RenderStaticIPv4Routes("t", []StaticRoute{
{Prefix: p, RouteBody: " bgp_community.add((65000,1));"},
})
if !strings.Contains(s, "route 203.0.113.0/24 unreachable {") {
t.Fatalf("missing block: %s", s)
}
if !strings.Contains(s, "bgp_community.add((65000,1))") {
t.Fatalf("missing community: %s", s)
}
}
@@ -2,6 +2,7 @@ router id 192.0.2.1;
include "bird.d/evobgp_filters_v4.conf";
include "bird.d/evobgp_filters_v6.conf";
include "bird.d/evobgp_bgp_template.conf";
include "bird.d/evobgp_prefixes_v4.conf";
include "bird.d/evobgp_prefixes_v6.conf";
include "bird.d/evobgp_peers.conf";
@@ -5,6 +5,7 @@ router id 192.0.2.1;
include "bird.d/evobgp_filters_v4.conf";
include "bird.d/evobgp_filters_v6.conf";
include "bird.d/evobgp_bgp_template.conf";
include "bird.d/evobgp_prefixes_v4.conf";
include "bird.d/evobgp_prefixes_v6.conf";
include "bird.d/evobgp_peers.conf";
@@ -0,0 +1,19 @@
template bgp bgp_template {
local as 65001;
ipv4 {
import none;
export filter evobgp_export_v4;
};
hold time 90;
keepalive time 30;
}
template bgp bgp_template_v6 {
local as 65001;
ipv6 {
import none;
export filter evobgp_export_v6;
};
hold time 90;
keepalive time 30;
}
@@ -1,8 +1,5 @@
protocol bgp evobgp_peer_ci {
local 192.0.2.1 as 65001;
protocol bgp evobgp_peer_ci from bgp_template {
neighbor 192.0.2.2 as 65002;
ipv4 {
import all;
export filter evobgp_export_v4;
};
multihop;
passive;
}
+3
View File
@@ -47,6 +47,9 @@ func BuildGzippedTar(revisionID, speakerID string, fragments map[string]string,
if p == "." || strings.HasPrefix(p, "..") {
return nil, fmt.Errorf("bundle: invalid path %q", p)
}
if strings.HasPrefix(p, "_") {
continue // UI-only preview aggregate, not shipped to nodes
}
norm[p] = content
}
+7 -1
View File
@@ -15,6 +15,11 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
// NewCDNHTTPClient returns the shared HTTP client for CDN and preview fetches (PERF-02 / ERR-03).
func NewCDNHTTPClient() *http.Client {
return &http.Client{Timeout: 45 * time.Second}
}
// BootstrapWorkers opens the same store.Backend and jobs.Registry as New (without HTTP or bundle keys).
// Used by standalone worker binaries (scheduler, ingest, …) that share PostgreSQL with the API.
func BootstrapWorkers(ctx context.Context, opts Options) (store.Backend, *jobs.Registry, *pgxpool.Pool, error) {
@@ -44,9 +49,10 @@ func BootstrapWorkers(ctx context.Context, opts Options) (store.Backend, *jobs.R
backend = mem
}
cdnHTTP := &http.Client{Timeout: 45 * time.Second}
cdnHTTP := NewCDNHTTPClient()
wk := &jobs.Worker{Store: backend, HTTPClient: cdnHTTP}
reg := jobs.NewRegistry(wk.Process)
wk.Registry = reg
observability.RegisterStoreBackend(backend)
return backend, reg, pool, nil
}
+24
View File
@@ -0,0 +1,24 @@
package httpapi
import (
"net/http"
"strconv"
)
func parseListLimit(r *http.Request) int {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
return 50
}
if limit > 500 {
return 500
}
return limit
}
func strPtrOrNull(s string) any {
if s == "" {
return nil
}
return s
}
+22
View File
@@ -2,9 +2,15 @@ package httpapi
import (
"encoding/json"
"log"
"net/http"
)
const (
internalErrorDetail = "an internal error occurred"
badGatewayDetail = "upstream request failed"
)
// Problem is RFC 9457 application/problem+json.
type Problem struct {
Type string `json:"type,omitempty"`
@@ -34,3 +40,19 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
func writeNoContent(w http.ResponseWriter) {
w.WriteHeader(http.StatusNoContent)
}
// writeInternalError logs err server-side and returns a generic 500 problem (ERR-01).
func writeInternalError(w http.ResponseWriter, operation string, err error) {
if err != nil {
log.Printf("httpapi: %s: %v", operation, err)
}
writeProblem(w, http.StatusInternalServerError, "Internal Error", internalErrorDetail)
}
// writeBadGateway logs err server-side and returns a generic 502 problem (ERR-01).
func writeBadGateway(w http.ResponseWriter, operation string, err error) {
if err != nil {
log.Printf("httpapi: %s: %v", operation, err)
}
writeProblem(w, http.StatusBadGateway, "Bad Gateway", badGatewayDetail)
}
+289 -32
View File
@@ -14,10 +14,14 @@ import (
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/bundle"
"evobgp/internal/jobs"
"evobgp/internal/observability"
"evobgp/internal/pipeline"
"evobgp/internal/reports"
"evobgp/internal/store"
"evobgp/internal/version"
)
// Handler returns the root HTTP handler (system routes public; rest under /v1/ authenticated).
@@ -27,6 +31,7 @@ func (s *Server) Handler() http.Handler {
wrappedV1 := http.StripPrefix("/v1", v1)
s.mux.Handle("GET /metrics", observability.MetricsHandler())
s.mux.HandleFunc("GET /version", s.handleVersion)
s.mux.HandleFunc("GET /v1/health", s.handleHealth)
s.mux.HandleFunc("GET /v1/ready", s.handleReady)
s.mux.HandleFunc("GET /v1/version", s.handleVersion)
@@ -46,18 +51,22 @@ func (s *Server) registerRoutes() {
func (s *Server) registerV1(m *http.ServeMux) {
m.HandleFunc("GET /modules", s.handleListModules)
m.HandleFunc("GET /router-lists/catalog", s.handleRouterListsCatalog)
m.HandleFunc("GET /modules/{module_id}", s.handleGetModule)
m.HandleFunc("GET /peers", s.handleListPeers)
m.HandleFunc("GET /speakers", s.handleListSpeakers)
m.HandleFunc("POST /modules/{module_id}/refresh", s.handleModuleRefresh)
m.HandleFunc("POST /tenant/refresh", s.handleTenantRefresh)
m.HandleFunc("GET /revisions", s.handleListRevisions)
m.HandleFunc("GET /revisions/{revision_id}", s.handleGetRevision)
m.HandleFunc("GET /revisions/{revision_id}/preview", s.handleRevisionPreview)
m.HandleFunc("GET /revisions/{revision_id}/diagnostic-log", s.handleRevisionDiagnosticLog)
m.HandleFunc("GET /revisions/{revision_a}/diff/{revision_b}", s.handleRevisionDiff)
m.HandleFunc("POST /revisions/{revision_id}/rollback", s.handleRevisionRollback)
m.HandleFunc("POST /apply", s.handleApply)
m.HandleFunc("POST /speakers/{id}/apply", s.handleSpeakerApply)
m.HandleFunc("POST /bird/reload", s.handleBirdReload)
m.HandleFunc("GET /bird/status", s.handleBirdStatus)
m.HandleFunc("GET /jobs", s.handleListJobs)
m.HandleFunc("GET /jobs/{job_id}", s.handleGetJob)
m.HandleFunc("POST /jobs/{job_id}/cancel", s.handleCancelJob)
@@ -77,7 +86,7 @@ func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
defer cancel()
if s.pgPool != nil {
if err := s.pgPool.Ping(ctx); err != nil {
checks["postgres"] = err.Error()
checks["postgres"] = "unavailable"
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"status": "not_ready", "checks": checks})
return
}
@@ -89,11 +98,16 @@ func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
sha := strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA"))
if sha == "" {
sha = "unknown"
ver, sha, buildTime := version.Info()
resp := map[string]string{
"version": ver,
"api_version": ver,
"git_sha": sha,
}
writeJSON(w, http.StatusOK, map[string]string{"api_version": "0.1.0", "git_sha": sha})
if buildTime != "" {
resp["build_time"] = buildTime
}
writeJSON(w, http.StatusOK, resp)
}
func moduleJSON(mod *store.Module) map[string]any {
@@ -106,11 +120,23 @@ func moduleJSON(mod *store.Module) map[string]any {
"refresh_interval_sec": mod.RefreshIntervalSec,
"cron_expr": mod.CronExpr,
}
if mod.LastRefreshedAt != nil {
m["last_refreshed_at"] = mod.LastRefreshedAt.UTC().Format(time.RFC3339Nano)
} else {
m["last_refreshed_at"] = nil
}
if mod.DefaultCommunityID != nil {
m["default_community_id"] = *mod.DefaultCommunityID
} else {
m["default_community_id"] = nil
}
ids := mod.EffectiveDohProfileIDs()
if len(ids) > 0 {
m["doh_profile_ids"] = ids
} else {
m["doh_profile_ids"] = []string{}
}
m["doh_resolver_policy"] = store.NormalizeDohResolverPolicy(mod.DohResolverPolicy)
if mod.DohProfileID != nil {
m["doh_profile_id"] = *mod.DohProfileID
} else {
@@ -159,13 +185,58 @@ func (s *Server) handleListModules(w http.ResponseWriter, r *http.Request) {
if !s.requireAtLeast(w, a, "viewer") {
return
}
mods := s.store.ListModules(a.TenantID)
items := make([]map[string]any, 0, len(mods))
for _, mod := range mods {
typeFilter := strings.TrimSpace(r.URL.Query().Get("type"))
enabledRaw := strings.TrimSpace(r.URL.Query().Get("enabled"))
var enabledFilter *bool
if enabledRaw != "" {
v, err := strconv.ParseBool(enabledRaw)
if err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "enabled must be boolean")
return
}
enabledFilter = &v
}
filtered := make([]*store.Module, 0)
for _, mod := range s.store.ListModules(a.TenantID) {
if typeFilter != "" && mod.Type != typeFilter {
continue
}
if enabledFilter != nil && mod.Enabled != *enabledFilter {
continue
}
filtered = append(filtered, mod)
}
page, next, more := store.PaginateOffset(filtered, r.URL.Query().Get("cursor"), parseListLimit(r))
items := make([]map[string]any, 0, len(page))
for _, mod := range page {
items = append(items, moduleJSON(mod))
}
writeJSON(w, http.StatusOK, map[string]any{
"items": items, "next_cursor": nil, "has_more": false,
"items": items, "next_cursor": strPtrOrNull(next), "has_more": more,
})
}
func (s *Server) handleRouterListsCatalog(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requireAtLeast(w, a, "viewer") {
return
}
cat, err := reports.BuildRouterListsCatalog(s.store, a.TenantID)
if err != nil {
writeStoreErr(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"modules": map[string]any{"items": cat.Modules},
"domains": map[string]any{"items": cat.Domains},
"asns": map[string]any{"items": cat.ASNs},
"ip_ranges": map[string]any{"items": cat.IPRanges},
"communities": map[string]any{"items": cat.Communities},
})
}
@@ -184,7 +255,7 @@ func (s *Server) handleGetModule(w http.ResponseWriter, r *http.Request) {
writeProblem(w, http.StatusNotFound, "Not Found", "module not found")
return
}
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
writeJSON(w, http.StatusOK, moduleJSON(mod))
@@ -199,16 +270,91 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) {
if !s.requireAtLeast(w, a, "viewer") {
return
}
peers := s.store.ListPeers(a.TenantID)
items := make([]map[string]any, 0, len(peers))
for _, p := range peers {
items = append(items, peerJSON(p))
allPeers := s.store.ListPeers(a.TenantID)
page, next, more := store.PaginateOffset(allPeers, r.URL.Query().Get("cursor"), parseListLimit(r))
liveStates := s.liveBGPProtocolStates(r.Context())
items := make([]map[string]any, 0, len(page))
for _, p := range page {
row := peerJSON(p)
if st, ok := liveStates[peerProtocolNameForID(p.ID)]; ok && strings.TrimSpace(st) != "" {
row["session_state"] = strings.TrimSpace(st)
}
items = append(items, row)
}
writeJSON(w, http.StatusOK, map[string]any{
"items": items, "next_cursor": nil, "has_more": false,
"items": items, "next_cursor": strPtrOrNull(next), "has_more": more,
})
}
func (s *Server) liveBGPProtocolStates(ctx context.Context) map[string]string {
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
if sock == "" {
return map[string]string{}
}
out, err := birdfmt.ShowProtocols(ctx, sock, strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN")))
if err != nil {
return map[string]string{}
}
return parseBGPProtocolStates(out)
}
// parseBGPProtocolStates parses `birdc show protocols all` summary rows into protocol_name -> state.
func parseBGPProtocolStates(output string) map[string]string {
out := make(map[string]string)
for _, raw := range strings.Split(output, "\n") {
line := strings.TrimSpace(raw)
if line == "" {
continue
}
low := strings.ToLower(line)
if strings.HasPrefix(low, "bird ") || strings.HasPrefix(low, "name ") || strings.HasPrefix(low, "table ") {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
if !strings.EqualFold(fields[1], "BGP") {
continue
}
state := extractBGPSessionState(line)
if state == "" {
state = fields[3]
}
out[fields[0]] = state
}
return out
}
func extractBGPSessionState(line string) string {
known := []string{
"Established",
"Idle",
"Connect",
"Active",
"OpenSent",
"OpenConfirm",
}
for _, st := range known {
if strings.Contains(line, st) {
return st
}
}
return ""
}
// peerProtocolNameForID must stay in sync with pipeline peer protocol naming.
func peerProtocolNameForID(peerID string) string {
s := strings.ReplaceAll(strings.TrimSpace(peerID), "-", "")
if len(s) > 16 {
s = s[:16]
}
if s == "" {
s = "x"
}
return "evobgp_p_" + s
}
func (s *Server) handleListSpeakers(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
@@ -244,11 +390,7 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
writeProblem(w, http.StatusNotFound, "Not Found", "module not found")
return
}
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
return
}
if mod.Type == "IP_RANGES" {
writeNoContent(w)
writeInternalError(w, "internal", err)
return
}
idem := r.Header.Get("Idempotency-Key")
@@ -260,7 +402,7 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
mid := mod.ID
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindModuleRefresh, idemPtr, &mid, map[string]any{"module_id": moduleID})
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
@@ -268,6 +410,60 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusAccepted, map[string]any{"job_id": snap["job_id"], "status": snap["status"]})
}
func (s *Server) handleTenantRefresh(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requireAtLeast(w, a, "editor") {
return
}
var body struct {
ModuleIDs []string `json:"module_ids"`
}
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
return
}
}
moduleIDs := body.ModuleIDs
if len(moduleIDs) == 0 {
now := time.Now().UTC()
for _, mod := range s.store.ListModules(a.TenantID) {
if pipeline.ModuleDueForScheduler(mod, now) {
moduleIDs = append(moduleIDs, mod.ID)
}
}
}
if len(moduleIDs) == 0 {
writeJSON(w, http.StatusNoContent, map[string]any{"message": "no modules due for refresh"})
return
}
idem := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
var idemPtr *string
if idem != "" {
idemPtr = &idem
}
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindTenantRefresh, idemPtr, nil, map[string]any{
"module_ids": moduleIDs,
"trigger": "api",
})
if err != nil {
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
snap := j.Snapshot()
writeJSON(w, http.StatusAccepted, map[string]any{
"job_id": snap["job_id"],
"status": snap["status"],
"module_ids": moduleIDs,
"modules_due": len(moduleIDs),
})
}
func (s *Server) handleListRevisions(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
@@ -313,11 +509,31 @@ func revisionJSON(rev *store.Revision) map[string]any {
return m
}
func strPtrOrNull(s string) any {
if s == "" {
return nil
// enqueueModuleRefreshIfEnabled queues module_refresh when the module exists and is enabled (best-effort, no HTTP error).
func (s *Server) enqueueModuleRefreshIfEnabled(tenantID, moduleID, trigger string) {
if s.jobs == nil {
return
}
return s
mod, err := s.store.GetModule(tenantID, moduleID)
if err != nil || !mod.Enabled {
return
}
mid := moduleID
_, _, _ = s.jobs.Enqueue(tenantID, jobs.KindModuleRefresh, nil, &mid, map[string]any{
"module_id": moduleID,
"trigger": trigger,
})
}
// enqueuePeerReconcile queues fast peer-only reconcile/render (best-effort, no HTTP error).
func (s *Server) enqueuePeerReconcile(tenantID, trigger string) {
if s.jobs == nil {
return
}
_, _, _ = s.jobs.Enqueue(tenantID, jobs.KindPeerReconcile, nil, nil, map[string]any{
"trigger": trigger,
"job_title": "Обновление BGP пиров",
})
}
func (s *Server) handleGetRevision(w http.ResponseWriter, r *http.Request) {
@@ -373,6 +589,32 @@ func (s *Server) handleRevisionPreview(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, obj)
}
func (s *Server) handleRevisionDiagnosticLog(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requireAtLeast(w, a, "viewer") {
return
}
revID := r.PathValue("revision_id")
rev, err := s.store.GetRevision(a.TenantID, revID)
if err != nil {
writeProblem(w, http.StatusNotFound, "Not Found", "revision not found")
return
}
logOut, err := reports.BuildRevisionDiagnosticLog(s.store, a.TenantID, rev)
if err != nil {
writeInternalError(w, "revision diagnostic log", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="`+logOut.Filename+`"`)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(logOut.Body)
}
func (s *Server) handleRevisionDiff(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
@@ -396,7 +638,7 @@ func (s *Server) handleRevisionRollback(w http.ResponseWriter, r *http.Request)
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requireAtLeast(w, a, "editor") {
if !s.requireAtLeast(w, a, "operator") {
return
}
revID := r.PathValue("revision_id")
@@ -413,7 +655,7 @@ func (s *Server) handleRevisionRollback(w http.ResponseWriter, r *http.Request)
"source_revision_id": revID,
})
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
@@ -460,7 +702,7 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
"strategy": body.Strategy,
})
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
@@ -511,7 +753,7 @@ func (s *Server) handleSpeakerApply(w http.ResponseWriter, r *http.Request) {
"speaker_id": spkID,
})
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
@@ -536,13 +778,28 @@ func (s *Server) handleBirdReload(w http.ResponseWriter, r *http.Request) {
}
j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindBirdReload, idemPtr, nil, nil)
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Location", "/v1/jobs/"+j.ID)
writeJSON(w, http.StatusAccepted, map[string]any{"job_id": j.ID, "status": "queued"})
}
func (s *Server) handleBirdStatus(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
if !s.requireAtLeast(w, a, "viewer") {
return
}
ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second)
defer cancel()
st := birdfmt.InspectLocalBird(ctx)
writeJSON(w, http.StatusOK, st)
}
func (s *Server) handleListJobs(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
@@ -656,7 +913,7 @@ func (s *Server) handleNodeBundle(w http.ResponseWriter, r *http.Request) {
}
tgz, err := bundle.BuildGzippedTar(rid, sid, rev.PreviewFragments, s.bundlePriv)
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
w.Header().Set("Content-Type", "application/gzip")
@@ -706,7 +963,7 @@ func (s *Server) handleNodeEnroll(w http.ResponseWriter, r *http.Request) {
}
patch := &store.SpeakerPatch{MetaJSON: &meta}
if _, err := s.store.UpdateSpeaker(a.TenantID, sid, patch); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "internal", err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
+357 -35
View File
@@ -1,10 +1,18 @@
package httpapi
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"evobgp/internal/importer"
"evobgp/internal/pipeline"
"evobgp/internal/store"
)
@@ -15,6 +23,7 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
m.HandleFunc("GET /modules/{module_id}/cdn-sources", s.handleListCDNSources)
m.HandleFunc("POST /modules/{module_id}/cdn-sources", s.handlePostCDNSource)
m.HandleFunc("POST /modules/{module_id}/cdn-sources/preview", s.handlePreviewCDNSource)
m.HandleFunc("PATCH /modules/{module_id}/cdn-sources/{source_id}", s.handlePatchCDNSource)
m.HandleFunc("DELETE /modules/{module_id}/cdn-sources/{source_id}", s.handleDeleteCDNSource)
@@ -32,6 +41,8 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
m.HandleFunc("POST /modules/{module_id}/ip-range-entries", s.handlePostIPRange)
m.HandleFunc("PATCH /modules/{module_id}/ip-range-entries/{entry_id}", s.handlePatchIPRange)
m.HandleFunc("DELETE /modules/{module_id}/ip-range-entries/{entry_id}", s.handleDeleteIPRange)
m.HandleFunc("GET /modules/{module_id}/entries.csv", s.handleExportModuleEntriesCSV)
m.HandleFunc("POST /modules/{module_id}/entries.csv", s.handleImportModuleEntriesCSV)
m.HandleFunc("GET /doh-profiles", s.handleListDoh)
m.HandleFunc("POST /doh-profiles", s.handlePostDoh)
@@ -66,14 +77,16 @@ func (s *Server) handlePostModule(w http.ResponseWriter, r *http.Request) {
return
}
var body struct {
Type string `json:"type"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
RefreshIntervalSec int `json:"refresh_interval_sec"`
CronExpr string `json:"cron_expr"`
DefaultCommunityID *string `json:"default_community_id"`
DohProfileID *string `json:"doh_profile_id"`
Type string `json:"type"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
RefreshIntervalSec int `json:"refresh_interval_sec"`
CronExpr string `json:"cron_expr"`
DefaultCommunityID *string `json:"default_community_id"`
DohProfileID *string `json:"doh_profile_id"`
DohProfileIDs []string `json:"doh_profile_ids"`
DohResolverPolicy string `json:"doh_resolver_policy"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
@@ -83,6 +96,7 @@ func (s *Server) handlePostModule(w http.ResponseWriter, r *http.Request) {
Type: body.Type, Name: body.Name, Enabled: body.Enabled, Priority: body.Priority,
RefreshIntervalSec: body.RefreshIntervalSec, CronExpr: body.CronExpr,
DefaultCommunityID: body.DefaultCommunityID, DohProfileID: body.DohProfileID,
DohProfileIDs: body.DohProfileIDs, DohResolverPolicy: body.DohResolverPolicy,
})
if err != nil {
writeStoreErr(w, err)
@@ -96,11 +110,47 @@ func (s *Server) handlePatchModule(w http.ResponseWriter, r *http.Request) {
if !ok || !s.requireAtLeast(w, a, "editor") {
return
}
rawBody, err := io.ReadAll(r.Body)
if err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid body")
return
}
var body store.ModulePatch
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
if err := json.Unmarshal(rawBody, &body); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
// NOTE:
// In Go, unmarshalling JSON `null` into pointer fields results in nil,
// which is indistinguishable from "field omitted". For PATCH we need to
// distinguish these cases so clients can explicitly clear nullable fields.
var raw map[string]json.RawMessage
if err := json.Unmarshal(rawBody, &raw); err == nil {
if v, ok := raw["default_community_id"]; ok && string(v) == "null" {
empty := ""
body.DefaultCommunityID = &empty
}
if v, ok := raw["doh_profile_id"]; ok && string(v) == "null" {
empty := ""
body.DohProfileID = &empty
}
if v, ok := raw["doh_profile_ids"]; ok && string(v) == "null" {
empty := []string{}
body.DohProfileIDs = &empty
}
if v, ok := raw["doh_resolver_policy"]; ok && string(v) == "null" {
p := store.DohPolicyPrimaryOnly
body.DohResolverPolicy = &p
}
if v, ok := raw["cron_expr"]; ok && string(v) == "null" {
empty := ""
body.CronExpr = &empty
}
if v, ok := raw["refresh_interval_sec"]; ok && string(v) == "null" {
zero := 0
body.RefreshIntervalSec = &zero
}
}
mod, err := s.store.UpdateModule(a.TenantID, r.PathValue("module_id"), &body)
if err != nil {
writeStoreErr(w, err)
@@ -130,7 +180,7 @@ func writeStoreErr(w http.ResponseWriter, err error) {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", err.Error())
return
}
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
writeInternalError(w, "store", err)
}
func (s *Server) handleListCDNSources(w http.ResponseWriter, r *http.Request) {
@@ -151,7 +201,7 @@ func (s *Server) handleListCDNSources(w http.ResponseWriter, r *http.Request) {
}
func cdnSourceJSON(x *store.CDNSource) map[string]any {
m := map[string]any{"id": x.ID, "source_kind": x.SourceKind, "url": x.URL, "etag": x.Etag}
m := map[string]any{"id": x.ID, "source_kind": x.SourceKind, "url": x.URL, "prefix_path": x.PrefixPath, "etag": x.Etag}
if x.RefreshIntervalSec != nil {
m["refresh_interval_sec"] = *x.RefreshIntervalSec
} else {
@@ -162,9 +212,84 @@ func cdnSourceJSON(x *store.CDNSource) map[string]any {
} else {
m["community_id"] = nil
}
if x.LastRefreshedAt != nil {
m["last_refreshed_at"] = x.LastRefreshedAt.UTC().Format(time.RFC3339Nano)
} else {
m["last_refreshed_at"] = nil
}
return m
}
func (s *Server) handlePreviewCDNSource(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requireAtLeast(w, a, "editor") {
return
}
var body struct {
URL string `json:"url"`
SourceKind string `json:"source_kind"`
PrefixPath string `json:"prefix_path"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
u := strings.TrimSpace(body.URL)
if u == "" {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "url is required")
return
}
mod, err := s.store.GetModule(a.TenantID, r.PathValue("module_id"))
if err != nil {
writeStoreErr(w, err)
return
}
if mod.Type != "CDN_CIDRS" {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "module type must be CDN_CIDRS")
return
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, u, nil)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid url")
return
}
resp, err := s.cdnHTTP.Do(req)
if err != nil {
writeBadGateway(w, "cdn preview fetch", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
writeBadGateway(w, "cdn preview fetch", fmt.Errorf("upstream status: %s", resp.Status))
return
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
writeBadGateway(w, "cdn preview read body", err)
return
}
pfxs, err := pipeline.ExtractCIDRs(string(raw), body.SourceKind, body.PrefixPath)
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", err.Error())
return
}
items := make([]string, 0, len(pfxs))
const previewLimit = 100
for i, p := range pfxs {
if i >= previewLimit {
break
}
items = append(items, p.String())
}
writeJSON(w, http.StatusOK, map[string]any{
"items": items,
"total": len(pfxs),
"truncated": len(pfxs) > previewLimit,
"source_url": u,
})
}
func (s *Server) handlePostCDNSource(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requireAtLeast(w, a, "editor") {
@@ -175,11 +300,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))
}
@@ -193,11 +320,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))
}
@@ -206,10 +335,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)
}
@@ -231,22 +362,27 @@ func (s *Server) handleListAS(w http.ResponseWriter, r *http.Request) {
}
func asEntryJSON(x *store.ASEntry) map[string]any {
m := map[string]any{"id": x.ID}
if x.ASN != nil {
m["asn"] = *x.ASN
} else {
m["asn"] = nil
}
if x.Prefix != nil {
m["prefix"] = *x.Prefix
} else {
m["prefix"] = nil
}
m := map[string]any{"id": x.ID, "asn": x.ASN}
if x.CommunityID != nil {
m["community_id"] = *x.CommunityID
} else {
m["community_id"] = nil
}
if strings.TrimSpace(x.ASNName) != "" {
m["asn_name"] = strings.TrimSpace(x.ASNName)
} else {
m["asn_name"] = nil
}
if x.PrefixCount != nil {
m["prefix_count"] = *x.PrefixCount
} else {
m["prefix_count"] = nil
}
if x.ASNResolvedAt != nil {
m["asn_resolved_at"] = x.ASNResolvedAt.UTC().Format(time.RFC3339Nano)
} else {
m["asn_resolved_at"] = nil
}
return m
}
@@ -260,11 +396,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))
}
@@ -278,11 +416,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))
}
@@ -291,10 +431,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)
}
@@ -335,11 +477,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))
}
@@ -353,11 +497,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))
}
@@ -366,10 +512,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)
}
@@ -410,11 +558,13 @@ func (s *Server) handlePostIPRange(w http.ResponseWriter, r *http.Request) {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
x, err := s.store.CreateIPRangeEntry(a.TenantID, r.PathValue("module_id"), &body)
mid := r.PathValue("module_id")
x, err := s.store.CreateIPRangeEntry(a.TenantID, mid, &body)
if err != nil {
writeStoreErr(w, err)
return
}
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_create")
writeJSON(w, http.StatusCreated, ipRangeJSON(x))
}
@@ -428,11 +578,13 @@ func (s *Server) handlePatchIPRange(w http.ResponseWriter, r *http.Request) {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
x, err := s.store.UpdateIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
mid := r.PathValue("module_id")
x, err := s.store.UpdateIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
if err != nil {
writeStoreErr(w, err)
return
}
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_patch")
writeJSON(w, http.StatusOK, ipRangeJSON(x))
}
@@ -441,13 +593,142 @@ func (s *Server) handleDeleteIPRange(w http.ResponseWriter, r *http.Request) {
if !ok || !s.requireAtLeast(w, a, "editor") {
return
}
if err := s.store.DeleteIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
mid := r.PathValue("module_id")
if err := s.store.DeleteIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
writeStoreErr(w, err)
return
}
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_delete")
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleExportModuleEntriesCSV(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requireAtLeast(w, a, "viewer") {
return
}
moduleID := r.PathValue("module_id")
mod, err := s.store.GetModule(a.TenantID, moduleID)
if err != nil {
writeStoreErr(w, err)
return
}
communities, err := s.store.ListCommunities(a.TenantID)
if err != nil {
writeStoreErr(w, err)
return
}
communityByID := make(map[string]string, len(communities))
for _, c := range communities {
communityByID[c.ID] = strings.TrimSpace(c.Community)
}
records := make([][]string, 0, 64)
switch mod.Type {
case "AS_PREFIXES":
records = append(records, []string{"asn", "community"})
list, err := s.store.ListASEntries(a.TenantID, moduleID)
if err != nil {
writeStoreErr(w, err)
return
}
for _, x := range list {
community := ""
if x.CommunityID != nil {
community = communityByID[*x.CommunityID]
}
records = append(records, []string{strconv.FormatInt(x.ASN, 10), community})
}
case "DOMAINS":
records = append(records, []string{"domain", "community"})
list, err := s.store.ListDomainEntries(a.TenantID, moduleID)
if err != nil {
writeStoreErr(w, err)
return
}
for _, x := range list {
community := ""
if x.CommunityID != nil {
community = communityByID[*x.CommunityID]
}
records = append(records, []string{x.FQDN, community})
}
case "IP_RANGES":
records = append(records, []string{"ipRange", "community"})
list, err := s.store.ListIPRangeEntries(a.TenantID, moduleID)
if err != nil {
writeStoreErr(w, err)
return
}
for _, x := range list {
community := ""
if x.CommunityID != nil {
community = communityByID[*x.CommunityID]
}
records = append(records, []string{x.Prefix, community})
}
default:
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="module-entries.csv"`)
w.WriteHeader(http.StatusOK)
cw := csv.NewWriter(w)
for _, rec := range records {
if err := cw.Write(rec); err != nil {
return
}
}
cw.Flush()
}
func (s *Server) handleImportModuleEntriesCSV(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requireAtLeast(w, a, "editor") {
return
}
moduleID := r.PathValue("module_id")
res, err := importer.ImportModuleEntriesCSV(s.store, a.TenantID, moduleID, r.Body)
if err != nil {
if errors.Is(err, store.ErrInvalidInput) {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv is empty")
return
}
if strings.Contains(err.Error(), "importer: invalid csv") {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid csv")
return
}
if strings.Contains(err.Error(), "importer: line") {
detail := strings.TrimPrefix(err.Error(), "importer: ")
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", detail)
return
}
if strings.Contains(err.Error(), "importer: csv import/export") {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
return
}
writeStoreErr(w, err)
return
}
if res.Imported > 0 {
switch res.ModuleType {
case "AS_PREFIXES":
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "as_entry_import_csv")
case "DOMAINS":
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "domain_entry_import_csv")
case "IP_RANGES":
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "ip_range_import_csv")
}
}
writeJSON(w, http.StatusOK, map[string]any{
"imported": res.Imported,
"module_type": res.ModuleType,
})
}
func (s *Server) handleListDoh(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requireAtLeast(w, a, "viewer") {
@@ -563,7 +844,7 @@ func commJSON(x *store.Community) map[string]any {
if err := json.Unmarshal([]byte(x.ValueJSON), &v); err != nil {
v = x.ValueJSON
}
return map[string]any{"id": x.ID, "name": x.Name, "kind": x.Kind, "value_json": v}
return map[string]any{"id": x.ID, "community": x.Community, "title": x.Title, "value_json": v}
}
func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) {
@@ -643,6 +924,7 @@ func (s *Server) handlePostPeer(w http.ResponseWriter, r *http.Request) {
writeStoreErr(w, err)
return
}
s.enqueuePeerReconcile(a.TenantID, "peer_create")
writeJSON(w, http.StatusCreated, peerJSON(x))
}
@@ -674,6 +956,7 @@ func (s *Server) handlePatchPeer(w http.ResponseWriter, r *http.Request) {
writeStoreErr(w, err)
return
}
s.enqueuePeerReconcile(a.TenantID, "peer_patch")
writeJSON(w, http.StatusOK, peerJSON(x))
}
@@ -686,6 +969,7 @@ func (s *Server) handleDeletePeer(w http.ResponseWriter, r *http.Request) {
writeStoreErr(w, err)
return
}
s.enqueuePeerReconcile(a.TenantID, "peer_delete")
w.WriteHeader(http.StatusNoContent)
}
@@ -785,9 +1069,47 @@ func (s *Server) handlePatchSettings(w http.ResponseWriter, r *http.Request) {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
if raw, ok := body["revision_retention_minutes"]; ok && raw != nil {
v, ok := parseRevisionRetentionMinutes(raw)
if !ok {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "revision_retention_minutes must be an integer in range 15..43200")
return
}
body["revision_retention_minutes"] = v
}
if err := s.store.PatchGlobalSettings(a.TenantID, body); err != nil {
writeStoreErr(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func parseRevisionRetentionMinutes(v any) (int, bool) {
const minMinutes = 15
const maxMinutes = 30 * 24 * 60
var out int
switch x := v.(type) {
case float64:
if x != float64(int(x)) {
return 0, false
}
out = int(x)
case int:
out = x
case int64:
out = int(x)
case string:
n, err := strconv.Atoi(strings.TrimSpace(x))
if err != nil {
return 0, false
}
out = n
default:
return 0, false
}
if out < minMinutes || out > maxMinutes {
return 0, false
}
return out, true
}
+105
View File
@@ -0,0 +1,105 @@
package httpapi
import (
"encoding/csv"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestModuleEntriesCSVImportExportIPRanges(t *testing.T) {
srv, err := New(Options{
InsecureDev: true,
SeedDemo: true,
})
if err != nil {
t.Fatal(err)
}
defer srv.Close()
tenant, _, modIP, _, _ := srv.Store().DemoIDs()
srv.apiKeys = parseAPIKeysSpec("opkey|" + tenant + "|operator")
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
client := ts.Client()
base := ts.URL
reqList, _ := http.NewRequest(http.MethodGet, base+"/v1/communities?limit=10", nil)
reqList.Header.Set("Authorization", "Bearer opkey")
respList, err := client.Do(reqList)
if err != nil {
t.Fatal(err)
}
defer respList.Body.Close()
if respList.StatusCode != http.StatusOK {
b, _ := io.ReadAll(respList.Body)
t.Fatalf("communities status %d: %s", respList.StatusCode, b)
}
var listBody struct {
Items []struct {
Community string `json:"community"`
} `json:"items"`
}
if err := json.NewDecoder(respList.Body).Decode(&listBody); err != nil {
t.Fatal(err)
}
if len(listBody.Items) == 0 {
t.Fatal("expected seeded community")
}
csvBody := "\"ipRange\",\"community\"\n\"10.254.1.254/32\",\"" + listBody.Items[0].Community + "\"\n\"160.79.104.0/23\",\"" + listBody.Items[0].Community + "\"\n"
reqImport, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modIP+"/entries.csv", strings.NewReader(csvBody))
reqImport.Header.Set("Authorization", "Bearer opkey")
reqImport.Header.Set("Content-Type", "text/csv")
respImport, err := client.Do(reqImport)
if err != nil {
t.Fatal(err)
}
defer respImport.Body.Close()
if respImport.StatusCode != http.StatusOK {
b, _ := io.ReadAll(respImport.Body)
t.Fatalf("import status %d: %s", respImport.StatusCode, b)
}
var importBody struct {
Imported int `json:"imported"`
}
if err := json.NewDecoder(respImport.Body).Decode(&importBody); err != nil {
t.Fatal(err)
}
if importBody.Imported != 2 {
t.Fatalf("expected imported=2, got %d", importBody.Imported)
}
reqExport, _ := http.NewRequest(http.MethodGet, base+"/v1/modules/"+modIP+"/entries.csv", nil)
reqExport.Header.Set("Authorization", "Bearer opkey")
respExport, err := client.Do(reqExport)
if err != nil {
t.Fatal(err)
}
defer respExport.Body.Close()
if respExport.StatusCode != http.StatusOK {
b, _ := io.ReadAll(respExport.Body)
t.Fatalf("export status %d: %s", respExport.StatusCode, b)
}
if got := respExport.Header.Get("Content-Type"); !strings.Contains(got, "text/csv") {
t.Fatalf("unexpected content-type: %q", got)
}
raw, err := io.ReadAll(respExport.Body)
if err != nil {
t.Fatal(err)
}
records, err := csv.NewReader(strings.NewReader(string(raw))).ReadAll()
if err != nil {
t.Fatal(err)
}
if len(records) < 3 {
t.Fatalf("expected at least 3 csv rows, got %d", len(records))
}
if records[0][0] != "ipRange" || records[0][1] != "community" {
t.Fatalf("unexpected header: %#v", records[0])
}
}
+2
View File
@@ -24,6 +24,7 @@ type Server struct {
apiKeys []apiKeyRecord
insecureDev bool
corsOrigins []string
cdnHTTP *http.Client
mux *http.ServeMux
}
@@ -67,6 +68,7 @@ func New(opts Options) (*Server, error) {
apiKeys: parseAPIKeysSpec(opts.APIKeys),
insecureDev: opts.InsecureDev && opts.SeedDemo,
corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins),
cdnHTTP: NewCDNHTTPClient(),
}
s.mux = http.NewServeMux()
s.registerRoutes()
+196 -3
View File
@@ -13,6 +13,7 @@ import (
"evobgp/internal/jobs"
"evobgp/internal/signing"
"evobgp/internal/version"
)
const testBundleSeed = "0101010101010101010101010101010101010101010101010101010101010101"
@@ -28,7 +29,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
}
defer srv.Close()
tenant, modCDN, modIP, rev, speaker := srv.Store().DemoIDs()
srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator")
srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator,edkey|" + tenant + "|editor")
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
@@ -68,7 +69,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
}
})
t.Run("refresh IP_RANGES no op", func(t *testing.T) {
t.Run("refresh IP_RANGES queues render job", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modIP+"/refresh", nil)
req.Header.Set("Authorization", "Bearer opkey")
resp, err := client.Do(req)
@@ -76,10 +77,17 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
if resp.StatusCode != http.StatusAccepted {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
var body struct {
JobID string `json:"job_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
waitJob(t, client, base, "opkey", body.JobID)
})
t.Run("refresh CDN queues job", func(t *testing.T) {
@@ -117,6 +125,34 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
}
})
t.Run("diagnostic log for revision", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, base+"/v1/revisions/"+rev+"/diagnostic-log", nil)
req.Header.Set("Authorization", "Bearer opkey")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "text/plain") {
t.Fatalf("unexpected content-type: %q", got)
}
raw, _ := io.ReadAll(resp.Body)
body := string(raw)
for _, needle := range []string{
"EvoBGP revision diagnostic log",
"revision_id=" + rev,
"## Raw rows",
} {
if !strings.Contains(body, needle) {
t.Fatalf("diagnostic log missing %q", needle)
}
}
})
t.Run("list modules peers speakers", func(t *testing.T) {
for _, path := range []string{"/v1/modules", "/v1/peers", "/v1/speakers"} {
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
@@ -142,6 +178,90 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
}
})
t.Run("modules filter by type", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, base+"/v1/modules?type=IP_RANGES", nil)
req.Header.Set("Authorization", "Bearer opkey")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
var body struct {
Items []struct {
Type string `json:"type"`
} `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body.Items) == 0 {
t.Fatalf("expected at least one IP_RANGES module")
}
for _, item := range body.Items {
if item.Type != "IP_RANGES" {
t.Fatalf("unexpected module type %q", item.Type)
}
}
})
t.Run("router lists catalog endpoint", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, base+"/v1/router-lists/catalog", nil)
req.Header.Set("Authorization", "Bearer opkey")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
var body struct {
Modules struct {
Items []map[string]any `json:"items"`
} `json:"modules"`
Domains struct {
Items []map[string]any `json:"items"`
} `json:"domains"`
ASNs struct {
Items []map[string]any `json:"items"`
} `json:"asns"`
IPRanges struct {
Items []map[string]any `json:"items"`
} `json:"ip_ranges"`
Communities struct {
Items []map[string]any `json:"items"`
} `json:"communities"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body.Modules.Items) == 0 {
t.Fatalf("expected modules in catalog")
}
if body.Communities.Items == nil {
t.Fatalf("expected communities.items field in catalog")
}
})
t.Run("rollback forbidden for editor", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, base+"/v1/revisions/"+rev+"/rollback", nil)
req.Header.Set("Authorization", "Bearer edkey")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d want 403: %s", resp.StatusCode, b)
}
})
t.Run("rollback queues job", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, base+"/v1/revisions/"+rev+"/rollback", nil)
req.Header.Set("Authorization", "Bearer opkey")
@@ -236,3 +356,76 @@ func waitJob(t *testing.T, client *http.Client, base, token, jobID string) {
}
t.Fatalf("job %s did not complete", jobID)
}
func TestParseBGPProtocolStates(t *testing.T) {
raw := `BIRD 2.16.2 ready.
name proto table state since info
device1 Device --- up 2026-04-09
evobgp_p_abcdef01 BGP master up 2026-04-09 Established
evobgp_p_01234567 BGP master start 2026-04-09 Connect
`
got := parseBGPProtocolStates(raw)
if got["evobgp_p_abcdef01"] != "Established" {
t.Fatalf("expected protocol state Established, got %q", got["evobgp_p_abcdef01"])
}
if got["evobgp_p_01234567"] != "Connect" {
t.Fatalf("expected protocol state Connect, got %q", got["evobgp_p_01234567"])
}
}
func TestVersionEndpoints(t *testing.T) {
srv, err := New(Options{
InsecureDev: true,
SeedDemo: true,
BundleSeedHex: testBundleSeed,
})
if err != nil {
t.Fatal(err)
}
defer srv.Close()
oldVer := version.Version
oldSHA := version.GitSHA
oldTime := version.BuildTime
version.Version = "1.2.3"
version.GitSHA = "abc123def456"
version.BuildTime = "2026-05-20T12:00:00Z"
t.Cleanup(func() {
version.Version = oldVer
version.GitSHA = oldSHA
version.BuildTime = oldTime
})
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
for _, path := range []string{"/version", "/v1/version"} {
t.Run(path, func(t *testing.T) {
resp, err := http.Get(ts.URL + path)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
var body map[string]string
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["version"] != "1.2.3" {
t.Fatalf("version=%q want 1.2.3", body["version"])
}
if body["api_version"] != "1.2.3" {
t.Fatalf("api_version=%q want 1.2.3", body["api_version"])
}
if body["git_sha"] != "abc123def456" {
t.Fatalf("git_sha=%q", body["git_sha"])
}
if body["build_time"] != "2026-05-20T12:00:00Z" {
t.Fatalf("build_time=%q", body["build_time"])
}
})
}
}
+147
View File
@@ -0,0 +1,147 @@
package importer
import (
"encoding/csv"
"fmt"
"io"
"strconv"
"strings"
"evobgp/internal/store"
)
// Result of importing module entries from CSV.
type Result struct {
Imported int
ModuleType string
}
// ImportModuleEntriesCSV parses and creates entries for AS_PREFIXES, DOMAINS, or IP_RANGES modules.
func ImportModuleEntriesCSV(st store.Backend, tenantID, moduleID string, body io.Reader) (*Result, error) {
mod, err := st.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
cr := csv.NewReader(io.LimitReader(body, 8<<20))
cr.TrimLeadingSpace = true
cr.FieldsPerRecord = -1
rows, err := cr.ReadAll()
if err != nil {
return nil, fmt.Errorf("importer: invalid csv: %w", err)
}
if len(rows) == 0 {
return nil, fmt.Errorf("importer: %w", store.ErrInvalidInput)
}
communities, err := st.ListCommunities(tenantID)
if err != nil {
return nil, err
}
communityIDByID := make(map[string]string, len(communities))
communityIDByValue := make(map[string]string, len(communities))
for _, c := range communities {
communityIDByID[c.ID] = c.ID
communityIDByValue[strings.TrimSpace(c.Community)] = c.ID
}
resolveCommunity := func(raw string, required bool) (*string, error) {
v := strings.TrimSpace(raw)
if v == "" {
if required {
return nil, fmt.Errorf("community is required")
}
return nil, nil
}
if id, ok := communityIDByID[v]; ok {
return &id, nil
}
if id, ok := communityIDByValue[v]; ok {
return &id, nil
}
return nil, fmt.Errorf("unknown community %q", v)
}
start := 0
if len(rows[0]) >= 2 {
key := strings.ToLower(strings.TrimSpace(rows[0][0]))
switch key {
case "asn", "domain", "iprange":
start = 1
}
}
imported := 0
switch mod.Type {
case "AS_PREFIXES":
for i := start; i < len(rows); i++ {
rec := rows[i]
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
continue
}
if len(rec) < 2 {
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
}
asn, err := strconv.ParseInt(strings.TrimSpace(rec[0]), 10, 64)
if err != nil || asn <= 0 {
return nil, fmt.Errorf("importer: line %d: invalid asn", i+1)
}
cid, err := resolveCommunity(rec[1], false)
if err != nil {
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
}
if _, err := st.CreateASEntry(tenantID, moduleID, &store.ASEntry{ASN: asn, CommunityID: cid}); err != nil {
return nil, err
}
imported++
}
case "DOMAINS":
for i := start; i < len(rows); i++ {
rec := rows[i]
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
continue
}
if len(rec) < 2 {
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
}
fqdn := strings.TrimSpace(rec[0])
if fqdn == "" {
return nil, fmt.Errorf("importer: line %d: domain is required", i+1)
}
cid, err := resolveCommunity(rec[1], false)
if err != nil {
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
}
if _, err := st.CreateDomainEntry(tenantID, moduleID, &store.DomainEntry{FQDN: fqdn, CommunityID: cid}); err != nil {
return nil, err
}
imported++
}
case "IP_RANGES":
for i := start; i < len(rows); i++ {
rec := rows[i]
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
continue
}
if len(rec) < 2 {
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
}
prefix := strings.TrimSpace(rec[0])
if prefix == "" {
return nil, fmt.Errorf("importer: line %d: ipRange is required", i+1)
}
cid, err := resolveCommunity(rec[1], true)
if err != nil {
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
}
if _, err := st.CreateIPRangeEntry(tenantID, moduleID, &store.IPRangeEntry{Prefix: prefix, CommunityID: cid}); err != nil {
return nil, err
}
imported++
}
default:
return nil, fmt.Errorf("importer: csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
}
return &Result{Imported: imported, ModuleType: mod.Type}, nil
}
+134 -21
View File
@@ -2,7 +2,10 @@ package jobs
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -20,17 +23,17 @@ const (
// Job is the API-facing job model (поля согласованы со схемой job_audit в миграциях; персистенция в БД пока не подключена).
type Job struct {
ID string
TenantID string
Kind string
Status string
IdempotencyKey *string
ModuleID *string
CreatedAt time.Time
StartedAt *time.Time
FinishedAt *time.Time
Error *string
ProgressPct *int16
ID string
TenantID string
Kind string
Status string
IdempotencyKey *string
ModuleID *string
CreatedAt time.Time
StartedAt *time.Time
FinishedAt *time.Time
Error *string
ProgressPct *int16
Meta map[string]any
cancelRequested bool
mu sync.Mutex
@@ -148,6 +151,25 @@ func (j *Job) Snapshot() map[string]any {
return m
}
var jobRegistryMaxJobsOnce sync.Once
var jobRegistryMaxJobs int
// registryMaxJobsFromEnv returns EVOBGP_JOB_REGISTRY_MAX_JOBS once (0 = без лимита, только завершённые джобы вытесняются).
func registryMaxJobsFromEnv() int {
jobRegistryMaxJobsOnce.Do(func() {
s := strings.TrimSpace(os.Getenv("EVOBGP_JOB_REGISTRY_MAX_JOBS"))
if s == "" {
return
}
n, err := strconv.Atoi(s)
if err != nil || n <= 0 {
return
}
jobRegistryMaxJobs = n
})
return jobRegistryMaxJobs
}
// 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).
@@ -171,11 +193,54 @@ func NewRegistry(workerStart func(j *Job)) *Registry {
}
}
// pruneTerminalIfOver удаляет самые старые завершённые джобы (succeeded/failed/cancelled), пока len(byID) > maxJobs.
func (r *Registry) pruneTerminalIfOver(maxJobs int) {
if r == nil || maxJobs <= 0 || len(r.byID) <= maxJobs {
return
}
type fin struct {
j *Job
t time.Time
}
var cands []fin
for _, j := range r.byID {
st := j.statusLocked()
if st != StatusSucceeded && st != StatusFailed && st != StatusCancelled {
continue
}
j.mu.Lock()
ft := j.FinishedAt
j.mu.Unlock()
if ft == nil {
continue
}
cands = append(cands, fin{j: j, t: *ft})
}
need := len(r.byID) - maxJobs
if need <= 0 || len(cands) == 0 {
return
}
sort.Slice(cands, func(i, j int) bool { return cands[i].t.Before(cands[j].t) })
if need > len(cands) {
need = len(cands)
}
for i := 0; i < need; i++ {
v := cands[i].j
delete(r.byID, v.ID)
if v.IdempotencyKey != nil && *v.IdempotencyKey != "" {
delete(r.byIdempo, idempoKey{tenant: v.TenantID, key: *v.IdempotencyKey})
}
}
}
// Enqueue creates a job or returns an existing one for the same idempotency key.
func (r *Registry) Enqueue(tenantID, kind string, idempotencyKey *string, moduleID *string, meta map[string]any) (*Job, bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
maxJobs := registryMaxJobsFromEnv()
r.pruneTerminalIfOver(maxJobs)
if idempotencyKey != nil && *idempotencyKey != "" {
k := idempoKey{tenant: tenantID, key: *idempotencyKey}
if existing, ok := r.byIdempo[k]; ok {
@@ -197,6 +262,7 @@ func (r *Registry) Enqueue(tenantID, kind string, idempotencyKey *string, module
r.byIdempo[idempoKey{tenant: tenantID, key: *idempotencyKey}] = j
}
r.byID[j.ID] = j
r.pruneTerminalIfOver(maxJobs)
if r.workerStart != nil {
go r.workerStart(j)
@@ -229,22 +295,32 @@ func (r *Registry) List(tenantID, statusFilter, kindFilter, cursor string, limit
if limit <= 0 {
limit = 50
}
// Build candidate set under r.mu, but read mutable status fields (j.Status) outside
// of r.mu so we don't race with Job.mu-protected writes (MarkRunning/Succeed/Fail).
r.mu.RLock()
var all []*Job
all := make([]*Job, 0, len(r.byID))
for _, j := range r.byID {
if j.TenantID != tenantID {
continue
if j.TenantID == tenantID {
all = append(all, j)
}
if statusFilter != "" && j.Status != statusFilter {
continue
}
if kindFilter != "" && j.Kind != kindFilter {
continue
}
all = append(all, j)
}
r.mu.RUnlock()
// Apply filters outside of r.mu to synchronize with Job.mu.
if statusFilter != "" || kindFilter != "" {
filtered := all[:0]
for _, j := range all {
if kindFilter != "" && j.Kind != kindFilter {
continue
}
if statusFilter != "" && j.statusLocked() != statusFilter {
continue
}
filtered = append(filtered, j)
}
all = filtered
}
sort.Slice(all, func(i, j int) bool {
return all[i].CreatedAt.After(all[j].CreatedAt)
})
@@ -267,6 +343,43 @@ func (r *Registry) List(tenantID, statusFilter, kindFilter, cursor string, limit
return all[off:end], next, hasMore
}
// CountOtherActiveModuleRefresh returns how many module_refresh jobs for the tenant are still
// queued or running, excluding excludeJobID (the current job). Used to batch deploy_apply.
func (r *Registry) CountOtherActiveModuleRefresh(tenantID, excludeJobID string) int {
return r.CountOtherActiveRefresh(tenantID, excludeJobID)
}
// CountOtherActiveRefresh counts queued/running module_refresh and tenant_refresh jobs for the tenant.
func (r *Registry) CountOtherActiveRefresh(tenantID, excludeJobID string) int {
if r == nil {
return 0
}
r.mu.RLock()
candidates := make([]*Job, 0, 8)
for _, j := range r.byID {
if j.TenantID != tenantID {
continue
}
if j.Kind != KindModuleRefresh && j.Kind != KindTenantRefresh {
continue
}
if j.ID == excludeJobID {
continue
}
candidates = append(candidates, j)
}
r.mu.RUnlock()
n := 0
for _, j := range candidates {
st := j.statusLocked()
if st == StatusQueued || st == StatusRunning {
n++
}
}
return n
}
func parseCursor(s string, off *int) error {
_, err := fmt.Sscanf(s, "%d", off)
return err
+432 -9
View File
@@ -2,9 +2,13 @@ package jobs
import (
"context"
"fmt"
"net/http"
"net/netip"
"os"
"sort"
"strings"
"sync"
"time"
"evobgp/internal/birddeploy"
@@ -14,17 +18,55 @@ import (
"evobgp/internal/store"
)
// mergeBirdPostApplyMeta attaches a birdc snapshot after deploy/reload (best-effort).
func mergeBirdPostApplyMeta(j *Job) {
if strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET")) == "" {
j.mergeMeta(map[string]any{"bird_post_apply_check": "skipped_no_birdc_socket"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
st := birdfmt.InspectLocalBird(ctx)
inner := map[string]any{
"bgp_established": st.BGPEstablished,
"bgp_sessions_total": st.BGPSessionsTotal,
}
if st.Error != "" {
inner["ok"] = false
inner["error"] = st.Error
} else {
inner["ok"] = true
}
j.mergeMeta(map[string]any{"bird_post_apply": inner})
}
const (
KindModuleRefresh = "module_refresh"
KindDeployApply = "deploy_apply"
KindModuleRefresh = "module_refresh"
KindTenantRefresh = "tenant_refresh"
KindPeerReconcile = "peer_reconcile"
KindDeployApply = "deploy_apply"
KindRevisionRollback = "revision_rollback"
KindBirdReload = "bird_reload"
KindBirdReload = "bird_reload"
)
// Worker executes queued jobs against store.Backend (memory or SQL).
type Worker struct {
Store store.Backend
HTTPClient *http.Client // optional; CDN refresh uses this (default 45s timeout).
Store store.Backend
HTTPClient *http.Client // optional; CDN refresh uses this (default 45s timeout).
// Registry is set after BootstrapWorkers creates the job queue; used to chain deploy_apply after refresh/rollback.
Registry *Registry
// refreshGate serializes deploy_apply gating after module_refresh per tenant (see finishModuleRefreshSuccess).
refreshGate sync.Map // map[string]*sync.Mutex
}
type revisionLogEntry struct {
Kind string `json:"kind"`
Source string `json:"source"`
Community string `json:"community"`
CommunityLabel string `json:"community_label"`
PrefixCount int `json:"prefix_count"`
Sample []string `json:"sample,omitempty"`
Message string `json:"message"`
}
var defaultWorkerHTTP = &http.Client{Timeout: 45 * time.Second}
@@ -60,13 +102,15 @@ func (w *Worker) Process(j *Job) {
j.Fail("missing module_id in job meta")
return
}
rev, err := pipeline.RefreshModule(context.Background(), w.Store, w.httpClient(), j.TenantID, mid)
if err != nil {
if err := pipeline.RefreshModuleIngest(context.Background(), w.Store, w.httpClient(), j.TenantID, mid); err != nil {
j.Fail(err.Error())
return
}
j.mergeMeta(map[string]any{"revision_id": rev})
j.Succeed()
w.finishModuleRefreshSuccess(j, mid)
case KindTenantRefresh:
w.runTenantRefresh(j)
case KindPeerReconcile:
w.runPeerReconcile(j)
case KindDeployApply:
w.runDeployApply(j)
case KindRevisionRollback:
@@ -85,12 +129,205 @@ func (w *Worker) Process(j *Job) {
j.Fail(err.Error())
return
}
mergeBirdPostApplyMeta(j)
j.Succeed()
default:
j.Fail("unknown job kind")
}
}
func (w *Worker) runPeerReconcile(j *Job) {
if w == nil || w.Store == nil {
j.Fail("worker not configured")
return
}
const peerJobTitle = "Обновление BGP пиров"
j.mergeMeta(map[string]any{"job_title": peerJobTitle})
var revID string
latest, _, _ := w.Store.ListRevisions(j.TenantID, "", "", 1)
triggerModuleID, err := w.peerTriggerModuleID(j.TenantID, latest)
if err != nil {
j.Fail(err.Error())
return
}
if len(latest) == 0 {
// First run fallback: render full tenant state once if no baseline revision exists yet.
rid, err := pipeline.RenderTenantRevision(context.Background(), w.Store, w.httpClient(), j.TenantID, triggerModuleID)
if err != nil {
j.Fail(err.Error())
return
}
revID = rid
} else {
baseRevID := latest[0].ID
rows := make([]store.PrefixRow, 0, 1024)
cursor := ""
for {
page, next, more := w.Store.ListRevisionPrefixes(j.TenantID, baseRevID, cursor, 2000)
rows = append(rows, page...)
if !more || strings.TrimSpace(next) == "" {
break
}
cursor = next
}
rid, err := pipeline.RenderTenantRevisionFromPrefixes(context.Background(), w.Store, w.httpClient(), j.TenantID, triggerModuleID, rows)
if err != nil {
j.Fail(err.Error())
return
}
revID = rid
}
j.mergeMeta(map[string]any{"revision_id": revID})
if entries, total, err := w.buildRevisionLogEntries(j.TenantID, revID); err == nil {
j.mergeMeta(map[string]any{
"log_entries": entries,
"log_total": total,
"log_generated": time.Now().UTC().Format(time.RFC3339Nano),
})
} else {
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
}
j.Succeed()
w.enqueueDeployAllSpeakers(j, j.TenantID, revID)
}
func (w *Worker) peerTriggerModuleID(tenantID string, latest []*store.Revision) (string, error) {
if len(latest) > 0 {
if mid := strings.TrimSpace(latest[0].ModuleID); mid != "" {
return mid, nil
}
}
for _, mod := range w.Store.ListModules(tenantID) {
if mod == nil || !mod.Enabled {
continue
}
if strings.TrimSpace(mod.ID) != "" {
return mod.ID, nil
}
}
for _, mod := range w.Store.ListModules(tenantID) {
if mod == nil {
continue
}
if strings.TrimSpace(mod.ID) != "" {
return mod.ID, nil
}
}
return "", fmt.Errorf("missing module_id for peer reconcile")
}
func (w *Worker) tenantRefreshMu(tenantID string) *sync.Mutex {
v, _ := w.refreshGate.LoadOrStore(tenantID, &sync.Mutex{})
return v.(*sync.Mutex)
}
// finishModuleRefreshSuccess marks the refresh job and, for the last active refresh in tenant,
// creates one aggregate revision and enqueues a single deploy_apply.
func (w *Worker) runTenantRefresh(j *Job) {
moduleIDs := moduleIDsFromJobMeta(j.Meta)
if len(moduleIDs) == 0 {
j.Fail("missing module_ids in job meta")
return
}
if err := pipeline.RefreshTenantModules(context.Background(), w.Store, w.httpClient(), j.TenantID, moduleIDs); err != nil {
j.Fail(err.Error())
return
}
trigger, err := pipeline.PickTenantRefreshTriggerModule(w.Store, j.TenantID, moduleIDs)
if err != nil {
j.Fail(err.Error())
return
}
j.mergeMeta(map[string]any{"module_ids": moduleIDs, "modules_refreshed": len(moduleIDs)})
w.finishModuleRefreshSuccess(j, trigger)
}
func moduleIDsFromJobMeta(meta map[string]any) []string {
if meta == nil {
return nil
}
raw, ok := meta["module_ids"]
if !ok {
return nil
}
switch v := raw.(type) {
case []string:
return v
case []any:
var out []string
for _, x := range v {
if s, ok := x.(string); ok && strings.TrimSpace(s) != "" {
out = append(out, strings.TrimSpace(s))
}
}
return out
default:
return nil
}
}
func (w *Worker) finishModuleRefreshSuccess(j *Job, triggerModuleID string) {
if w == nil || w.Store == nil {
j.Succeed()
return
}
mu := w.tenantRefreshMu(j.TenantID)
mu.Lock()
defer mu.Unlock()
deferDeploy := false
if w.Registry != nil {
deferDeploy = w.Registry.CountOtherActiveRefresh(j.TenantID, j.ID) > 0
}
if deferDeploy {
j.mergeMeta(map[string]any{
"deploy_apply_deferred": true,
"deploy_apply_defer_reason": "parallel_module_refresh",
})
j.Succeed()
return
}
rev, err := pipeline.RenderTenantRevision(context.Background(), w.Store, w.httpClient(), j.TenantID, triggerModuleID)
if err != nil {
j.Fail(err.Error())
return
}
j.mergeMeta(map[string]any{"revision_id": rev})
if entries, total, err := w.buildRevisionLogEntries(j.TenantID, rev); err == nil {
j.mergeMeta(map[string]any{
"log_entries": entries,
"log_total": total,
"log_generated": time.Now().UTC().Format(time.RFC3339Nano),
})
} else {
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
}
j.Succeed()
w.enqueueDeployAllSpeakers(j, j.TenantID, rev)
}
// enqueueDeployAllSpeakers queues the same work as POST /v1/apply (all speakers, no speaker_id).
func (w *Worker) enqueueDeployAllSpeakers(j *Job, tenantID, revID string) {
if w == nil || w.Registry == nil {
return
}
revID = strings.TrimSpace(revID)
if revID == "" {
return
}
applyJob, _, err := w.Registry.Enqueue(tenantID, KindDeployApply, nil, nil, map[string]any{
"revision_id": revID,
})
if err != nil {
j.mergeMeta(map[string]any{"deploy_apply_enqueue_error": err.Error()})
return
}
if applyJob != nil {
j.mergeMeta(map[string]any{"deploy_apply_job_id": applyJob.ID})
}
}
func (w *Worker) runDeployApply(j *Job) {
revID, _ := j.Meta["revision_id"].(string)
spk, hasSpeaker := j.Meta["speaker_id"].(string)
@@ -122,6 +359,7 @@ func (w *Worker) runDeployApply(j *Job) {
return
}
}
applied := make([]string, 0, 8)
applyOne := func(speakerID string) error {
if err := w.Store.SetLastAppliedRevision(j.TenantID, speakerID, revID); err != nil {
return err
@@ -130,6 +368,7 @@ func (w *Worker) runDeployApply(j *Job) {
if err := w.Store.PublishRevisionForSpeaker(speakerID, revID); err != nil {
return err
}
applied = append(applied, speakerID)
return nil
}
if hasSpeaker && spk != "" {
@@ -137,6 +376,7 @@ func (w *Worker) runDeployApply(j *Job) {
j.Fail(err.Error())
return
}
mergeBirdPostApplyMeta(j)
j.Succeed()
return
}
@@ -146,6 +386,15 @@ func (w *Worker) runDeployApply(j *Job) {
return
}
}
j.mergeMeta(map[string]any{
"apply_summary": map[string]any{
"revision_id": revID,
"speakers_count": len(applied),
"speaker_ids": applied,
"message": fmt.Sprintf("Ревизия %s применена на %d спикерах", shortID(revID), len(applied)),
},
})
mergeBirdPostApplyMeta(j)
j.Succeed()
}
@@ -161,5 +410,179 @@ func (w *Worker) runRollback(j *Job) {
return
}
j.mergeMeta(map[string]any{"new_revision_id": newID})
j.mergeMeta(map[string]any{
"rollback_summary": map[string]any{
"source_revision_id": src,
"new_revision_id": newID,
"message": fmt.Sprintf("Откат %s → %s", shortID(src), shortID(newID)),
},
})
w.enqueueDeployAllSpeakers(j, j.TenantID, newID)
j.Succeed()
}
// buildCommunityLabelMap maps community UUID -> human-readable title (or BGP community string).
func buildCommunityLabelMap(st store.Backend, tenantID string) map[string]string {
out := make(map[string]string)
if st == nil {
return out
}
list, err := st.ListCommunities(tenantID)
if err != nil || list == nil {
return out
}
for _, c := range list {
if c == nil {
continue
}
label := strings.TrimSpace(c.Title)
if label == "" {
label = strings.TrimSpace(c.Community)
}
if label == "" {
label = c.ID
}
out[c.ID] = label
}
return out
}
func resolveCommunityLabel(commID string, byID map[string]string) string {
if commID == "" || commID == "none" {
return "без community"
}
if lbl, ok := byID[commID]; ok && strings.TrimSpace(lbl) != "" {
return strings.TrimSpace(lbl)
}
return commID
}
func (w *Worker) buildRevisionLogEntries(tenantID, revID string) ([]map[string]any, int, error) {
if w == nil || w.Store == nil {
return nil, 0, fmt.Errorf("store not configured")
}
commLabels := buildCommunityLabelMap(w.Store, tenantID)
type agg struct {
kind string
source string
community string
count int
sample []string
}
groups := map[string]*agg{}
total := 0
cursor := ""
for {
rows, next, more := w.Store.ListRevisionPrefixes(tenantID, revID, cursor, 1000)
for _, p := range rows {
total++
src := strings.TrimSpace(p.Source)
comm := "none"
if p.CommunityID != nil && strings.TrimSpace(*p.CommunityID) != "" {
comm = strings.TrimSpace(*p.CommunityID)
}
kind, sourceName := classifySource(src)
k := kind + "|" + sourceName + "|" + comm
g, ok := groups[k]
if !ok {
g = &agg{kind: kind, source: sourceName, community: comm}
groups[k] = g
}
g.count++
if len(g.sample) < 5 {
g.sample = append(g.sample, p.Prefix)
}
}
if !more {
break
}
cursor = next
if strings.TrimSpace(cursor) == "" {
break
}
}
keys := make([]string, 0, len(groups))
for k := range groups {
keys = append(keys, k)
}
sort.Strings(keys)
out := make([]map[string]any, 0, len(keys))
for _, k := range keys {
g := groups[k]
cl := resolveCommunityLabel(g.community, commLabels)
msg := humanLogMessage(g.kind, g.source, g.count, cl, g.sample)
out = append(out, map[string]any{
"kind": g.kind,
"source": g.source,
"community": g.community,
"community_label": cl,
"prefix_count": g.count,
"sample": g.sample,
"message": msg,
})
}
return out, total, nil
}
func classifySource(src string) (kind, name string) {
switch {
case strings.HasPrefix(src, "as:"):
return "asn", strings.TrimPrefix(src, "as:")
case strings.HasPrefix(src, "domain:"):
return "domain", strings.TrimPrefix(src, "domain:")
case strings.HasPrefix(src, "cdn:"):
return "cdn", strings.TrimPrefix(src, "cdn:")
case src == "ip_range":
return "ip_range", "manual_ranges"
default:
if src == "" {
return "unknown", "unknown"
}
return "source", src
}
}
// humanLogMessage builds a Russian log line; communityLabel is already resolved (title or BGP value).
func humanLogMessage(kind, source string, count int, communityLabel string, sample []string) string {
switch kind {
case "asn":
return fmt.Sprintf("AS%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
case "domain":
ips := strings.Join(prettyDomainSample(sample), " ")
if ips == "" {
ips = "—"
}
return fmt.Sprintf("%s: IP (%s) → добавлено в сообщество «%s»", source, ips, communityLabel)
case "cdn":
return fmt.Sprintf("CDN «%s»: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
case "ip_range":
return fmt.Sprintf("Статические диапазоны: добавлено %d префиксов в сообщество «%s»", count, communityLabel)
default:
return fmt.Sprintf("%s: добавлено %d префиксов в сообщество «%s»", source, count, communityLabel)
}
}
func prettyDomainSample(sample []string) []string {
out := make([]string, 0, len(sample))
for _, s := range sample {
p, err := netip.ParsePrefix(strings.TrimSpace(s))
if err != nil {
out = append(out, s)
continue
}
if (p.Addr().Is4() && p.Bits() == 32) || (p.Addr().Is6() && p.Bits() == 128) {
out = append(out, p.Addr().String())
continue
}
out = append(out, s)
}
return out
}
func shortID(id string) string {
s := strings.TrimSpace(id)
if len(s) <= 8 {
return s
}
return s[:8]
}
+178
View File
@@ -0,0 +1,178 @@
package jobs
import (
"testing"
"time"
"evobgp/internal/store"
)
func TestParallelModuleRefresh_CoalescesDeployApply(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
t.Setenv("EVOBGP_BIRD_ACTIVE_DIR", "") // skip bird binary path in deploy_apply
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
mod2, err := m.CreateModule(tenant, &store.Module{Type: "IP_RANGES", Name: "extra-ip", Enabled: true, Priority: 30})
if err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.0.0.0/24"}); err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "192.168.0.0/24"}); err != nil {
t.Fatal(err)
}
wk := &Worker{Store: m}
reg := NewRegistry(wk.Process)
wk.Registry = reg
mid1 := modIP
mid2 := mod2.ID
if _, _, err := reg.Enqueue(tenant, KindModuleRefresh, nil, &mid1, map[string]any{"module_id": modIP}); err != nil {
t.Fatal(err)
}
if _, _, err := reg.Enqueue(tenant, KindModuleRefresh, nil, &mid2, map[string]any{"module_id": mod2.ID}); err != nil {
t.Fatal(err)
}
waitSucceededJobsByKindCount(t, reg, tenant, KindModuleRefresh, 2)
deployJobs, _, _ := reg.List(tenant, "", KindDeployApply, "", 100)
if len(deployJobs) != 1 {
t.Fatalf("want exactly one deploy_apply job, got %d", len(deployJobs))
}
var deferred, withDeployID int
refreshJobs, _, _ := reg.List(tenant, "", KindModuleRefresh, "", 100)
for _, j := range refreshJobs {
if j == nil {
continue
}
snap := j.Snapshot()
if st, _ := snap["status"].(string); st != StatusSucceeded {
continue
}
meta, _ := snap["meta"].(map[string]any)
if v, ok := meta["deploy_apply_deferred"].(bool); ok && v {
deferred++
}
if _, ok := meta["deploy_apply_job_id"].(string); ok {
withDeployID++
}
}
if deferred != 1 || withDeployID != 1 {
t.Fatalf("want one deferred and one with deploy_apply_job_id, got deferred=%d deploy_meta=%d", deferred, withDeployID)
}
}
func waitSucceededModuleRefreshCount(t *testing.T, reg *Registry, tenant string, want int) {
t.Helper()
waitSucceededJobsByKindCount(t, reg, tenant, KindModuleRefresh, want)
}
func waitSucceededJobsByKindCount(t *testing.T, reg *Registry, tenant, kind string, want int) {
t.Helper()
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
jobs, _, _ := reg.List(tenant, StatusSucceeded, kind, "", 100)
if len(jobs) >= want {
return
}
all, _, _ := reg.List(tenant, "", kind, "", 100)
for _, j := range all {
if j == nil {
continue
}
snap := j.Snapshot()
if st, _ := snap["status"].(string); st != StatusFailed {
continue
}
t.Fatalf("%s job failed: %#v", kind, snap["error"])
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("timeout waiting for %s jobs", kind)
}
func TestPeerReconcile_RendersFromLatestRevisionAndQueuesDeploy(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
t.Setenv("EVOBGP_BIRD_ACTIVE_DIR", "") // skip bird binary path in deploy_apply
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.10.0.0/24"}); err != nil {
t.Fatal(err)
}
peer, err := m.CreatePeer(tenant, &store.BGPPeer{
Name: "peer-a",
Neighbor: "192.0.2.2",
RemoteASN: 64512,
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
wk := &Worker{Store: m}
reg := NewRegistry(wk.Process)
wk.Registry = reg
// Create baseline revision from enabled IP module.
mid := modIP
if _, _, err := reg.Enqueue(tenant, KindModuleRefresh, nil, &mid, map[string]any{"module_id": modIP}); err != nil {
t.Fatal(err)
}
waitSucceededJobsByKindCount(t, reg, tenant, KindModuleRefresh, 1)
beforeRevs, _, _ := m.ListRevisions(tenant, "", "", 200)
enabled := false
if _, err := m.UpdatePeer(tenant, peer.ID, &store.PeerPatch{Enabled: &enabled}); err != nil {
t.Fatal(err)
}
if _, _, err := reg.Enqueue(tenant, KindPeerReconcile, nil, nil, map[string]any{"trigger": "peer_patch"}); err != nil {
t.Fatal(err)
}
waitSucceededJobsByKindCount(t, reg, tenant, KindPeerReconcile, 1)
afterRevs, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(afterRevs) <= len(beforeRevs) {
t.Fatalf("expected new revision after peer_reconcile, before=%d after=%d", len(beforeRevs), len(afterRevs))
}
peerJobs, _, _ := reg.List(tenant, StatusSucceeded, KindPeerReconcile, "", 10)
if len(peerJobs) == 0 {
t.Fatal("expected succeeded peer_reconcile job")
}
meta := peerJobs[0].Snapshot()["meta"].(map[string]any)
if _, ok := meta["revision_id"].(string); !ok {
t.Fatalf("expected revision_id in peer_reconcile meta, got %#v", meta)
}
if title, ok := meta["job_title"].(string); !ok || title != "Обновление BGP пиров" {
t.Fatalf("expected job_title in peer_reconcile meta, got %#v", meta["job_title"])
}
if _, ok := meta["deploy_apply_job_id"].(string); !ok {
t.Fatalf("expected deploy_apply_job_id in peer_reconcile meta, got %#v", meta)
}
}
+214
View File
@@ -0,0 +1,214 @@
package nodecli
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/bundle"
"evobgp/internal/signing"
)
// CmdPullBundle downloads a signed bundle from the control plane API.
func CmdPullBundle(args []string) int {
fs := flag.NewFlagSet("pull-bundle", flag.ExitOnError)
base := fs.String("base-url", "", "control plane base URL, e.g. http://localhost:8080")
token := fs.String("token", "", "Bearer token (node role)")
speaker := fs.String("speaker-id", "", "bgp_speaker id")
revision := fs.String("revision-id", "", "revision to fetch (empty = latest pointer)")
out := fs.String("o", "bundle.tar.gz", "output file")
_ = fs.Parse(args)
if strings.TrimSpace(*base) == "" || *token == "" || *speaker == "" {
fmt.Fprintln(os.Stderr, "pull-bundle: -base-url, -token, -speaker-id are required")
return 2
}
rev := strings.TrimSpace(*revision)
if rev == "" {
var err error
rev, err = fetchLatestRevision(*base, *token, *speaker)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
}
body, err := fetchBundle(*base, *token, *speaker, rev)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := os.WriteFile(*out, body, 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "wrote %s (revision %s)\n", *out, rev)
return 0
}
func fetchLatestRevision(base, token, speaker string) (string, error) {
u := strings.TrimRight(base, "/") + "/v1/speakers/" + speaker + "/revisions/latest"
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("latest revision: %s: %s", resp.Status, strings.TrimSpace(string(b)))
}
var out struct {
RevisionID string `json:"revision_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
if out.RevisionID == "" {
return "", fmt.Errorf("empty revision_id in response")
}
return out.RevisionID, nil
}
func fetchBundle(base, token, speaker, revision string) ([]byte, error) {
u := strings.TrimRight(base, "/") + "/v1/speakers/" + speaker + "/bundle/" + revision
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("bundle: %s: %s", resp.Status, strings.TrimSpace(string(b)))
}
return io.ReadAll(resp.Body)
}
func loadPubKey(pubB64, pubHex string) (ed25519.PublicKey, error) {
switch {
case strings.TrimSpace(pubB64) != "":
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(pubB64))
if err != nil {
return nil, err
}
if len(raw) != ed25519.PublicKeySize {
return nil, fmt.Errorf("pubkey-base64 must decode to %d bytes", ed25519.PublicKeySize)
}
return ed25519.PublicKey(raw), nil
case strings.TrimSpace(pubHex) != "":
return bundle.ParsePublicKeyHex(pubHex)
default:
return nil, fmt.Errorf("public key required")
}
}
// CmdVerifyBundle checks bundle signature and manifest.
func CmdVerifyBundle(args []string) int {
fs := flag.NewFlagSet("verify-bundle", flag.ExitOnError)
path := fs.String("f", "", "path to bundle.tar.gz")
pubB64 := fs.String("pubkey-base64", "", "Ed25519 public key (base64)")
pubHex := fs.String("pubkey-hex", "", "Ed25519 public key (64 hex chars)")
_ = fs.Parse(args)
if *path == "" {
fmt.Fprintln(os.Stderr, "verify-bundle: -f required")
return 2
}
pub, err := loadPubKey(*pubB64, *pubHex)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
raw, err := os.ReadFile(*path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
v, err := signing.VerifyGzippedTar(raw, pub)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "ok: revision %s, %d payload files\n", v.Manifest.RevisionID, len(v.Files))
return 0
}
// CmdApplyBundle verifies, extracts, parse-checks, and configures BIRD from a bundle.
func CmdApplyBundle(args []string) int {
fs := flag.NewFlagSet("apply-bundle", flag.ExitOnError)
path := fs.String("f", "", "path to bundle.tar.gz")
dir := fs.String("extract-dir", "", "directory to extract into")
pubB64 := fs.String("pubkey-base64", "", "Ed25519 public key (base64)")
pubHex := fs.String("pubkey-hex", "", "Ed25519 public key (64 hex chars)")
bird := fs.String("bird", "", "bird binary (default PATH)")
birdc := fs.String("birdc", "", "birdc binary (default PATH)")
socket := fs.String("socket", "", "birdc -s socket path")
timeout := fs.Duration("timeout", 30*time.Second, "timeout for bird/birdc")
_ = fs.Parse(args)
if *path == "" || *dir == "" {
fmt.Fprintln(os.Stderr, "apply-bundle: -f and -extract-dir required")
return 2
}
pub, err := loadPubKey(*pubB64, *pubHex)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
raw, err := os.ReadFile(*path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
v, err := signing.VerifyGzippedTar(raw, pub)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
root := filepath.Clean(*dir)
if err := os.MkdirAll(root, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := bundle.WriteExtractedFiles(root, v); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
mainRel := v.FindMainBirdConf()
if mainRel == "" {
fmt.Fprintln(os.Stderr, "bundle has no bird.conf path in manifest")
return 1
}
mainPath := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(mainRel, "/")))
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
ctl := &birdfmt.BirdCtl{Bird: *bird, Birdc: *birdc, Socket: *socket}
if err := ctl.ParseCheck(ctx, mainPath); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := ctl.Configure(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Fprintf(os.Stderr, "applied revision %s (main config %s)\n", v.Manifest.RevisionID, mainPath)
return 0
}
+38
View File
@@ -0,0 +1,38 @@
// Package nodecli implements evobgp-node subcommands (pull/verify/apply bundle).
package nodecli
import (
"fmt"
"os"
)
// Run executes a subcommand from args (without program name). Returns exit code.
func Run(args []string) int {
if len(args) < 1 {
Usage(os.Stderr)
return 2
}
switch args[0] {
case "pull-bundle":
return CmdPullBundle(args[1:])
case "verify-bundle":
return CmdVerifyBundle(args[1:])
case "apply-bundle":
return CmdApplyBundle(args[1:])
default:
Usage(os.Stderr)
return 2
}
}
// Usage prints CLI help to w.
func Usage(w interface{ Write([]byte) (int, error) }) {
fmt.Fprintf(w, `Usage:
evobgp-node pull-bundle -base-url URL -token TOKEN -speaker-id ID [-revision-id ID] [-o path]
evobgp-node verify-bundle -f bundle.tar.gz (-pubkey-base64 B64 | -pubkey-hex HEX)
evobgp-node apply-bundle -f bundle.tar.gz -extract-dir DIR (-pubkey-base64 B64 | -pubkey-hex HEX)
[-bird PATH] [-birdc PATH] [-socket PATH] [-timeout DURATION]
apply-bundle verifies, extracts, runs bird -p on main bird.conf, then birdc configure.
`)
}
+46 -6
View File
@@ -59,8 +59,42 @@ var (
Name: "build_info",
Help: "Build metadata (value always 1).",
}, []string{"version", "git_sha"})
prefixAggregationDuration = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Name: "prefix_aggregation_duration_seconds",
Help: "Time spent in smartAggregatePrefixRows during tenant render.",
Buckets: prometheus.ExponentialBuckets(0.0001, 2, 16),
})
prefixAggregationRawCount = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Name: "prefix_aggregation_raw_count",
Help: "Prefix row count before CIDR aggregation on tenant render.",
Buckets: prometheus.ExponentialBuckets(1, 2, 16),
})
prefixAggregationAggregatedCount = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Name: "prefix_aggregation_aggregated_count",
Help: "Prefix row count after CIDR aggregation on tenant render.",
Buckets: prometheus.ExponentialBuckets(1, 2, 16),
})
)
// RecordPrefixAggregation records tenant render CIDR aggregation stats.
func RecordPrefixAggregation(rawCount, aggregatedCount int, duration time.Duration) {
if rawCount < 0 {
rawCount = 0
}
if aggregatedCount < 0 {
aggregatedCount = 0
}
prefixAggregationDuration.Observe(duration.Seconds())
prefixAggregationRawCount.Observe(float64(rawCount))
prefixAggregationAggregatedCount.Observe(float64(aggregatedCount))
}
// RecordJobTerminal increments jobs_finished_total for terminal statuses.
func RecordJobTerminal(kind, status string) {
switch status {
@@ -196,14 +230,15 @@ func (s *statusRecorder) WriteHeader(code int) {
}
// StartBirdProtocolsPoller runs birdc "show protocols" on interval when socket is non-empty.
func StartBirdProtocolsPoller(socket string, birdcPath string, interval time.Duration, showFn func(ctx context.Context, socket, birdcBin string) (string, error), countFn func(output string) int) {
// Горутина завершается при отмене ctx (корректное завершение вместе с процессом API).
func StartBirdProtocolsPoller(ctx context.Context, socket string, birdcPath string, interval time.Duration, showFn func(ctx context.Context, socket, birdcBin string) (string, error), countFn func(output string) int) {
socket = trimSpace(socket)
if socket == "" || interval <= 0 || showFn == nil || countFn == nil {
if ctx == nil || socket == "" || interval <= 0 || showFn == nil || countFn == nil {
return
}
scrape := func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
out, err := showFn(ctx, socket, birdcPath)
sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
out, err := showFn(sctx, socket, birdcPath)
cancel()
if err != nil {
SetBirdSessionMetrics(0, false)
@@ -215,8 +250,13 @@ func StartBirdProtocolsPoller(socket string, birdcPath string, interval time.Dur
scrape()
t := time.NewTicker(interval)
defer t.Stop()
for range t.C {
scrape()
for {
select {
case <-ctx.Done():
return
case <-t.C:
scrape()
}
}
}()
}
+106
View File
@@ -0,0 +1,106 @@
package pipeline
import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"evobgp/internal/store"
)
func collectConcurrency() int {
n := 8
if s := strings.TrimSpace(os.Getenv("EVOBGP_COLLECT_CONCURRENCY")); s != "" {
if v, err := strconv.Atoi(s); err == nil && v > 0 {
n = v
}
}
if n > 32 {
n = 32
}
return n
}
// aggregateTenantPrefixRowsAll builds the union of materialized prefixes for all enabled modules.
// Uses per-module snapshots when inputs are unchanged to avoid duplicate external fetches on render.
func aggregateTenantPrefixRowsAll(ctx context.Context, st store.Backend, hc *http.Client, tenantID string) ([]store.PrefixRow, error) {
mods := st.ListModules(tenantID)
type modRef struct {
id string
}
var enabled []modRef
for _, m := range mods {
if m != nil && m.Enabled {
enabled = append(enabled, modRef{id: m.ID})
}
}
if len(enabled) == 0 {
return nil, nil
}
if len(enabled) == 1 {
omod, err := st.GetModule(tenantID, enabled[0].id)
if err != nil {
return nil, err
}
return rowsForModule(ctx, st, hc, tenantID, omod)
}
sem := make(chan struct{}, collectConcurrency())
results := make([][]store.PrefixRow, len(enabled))
errs := make([]error, len(enabled))
var wg sync.WaitGroup
for i, ref := range enabled {
wg.Add(1)
go func(idx int, moduleID string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
omod, err := st.GetModule(tenantID, moduleID)
if err != nil {
errs[idx] = err
return
}
rows, err := rowsForModule(ctx, st, hc, tenantID, omod)
if err != nil {
errs[idx] = fmt.Errorf("module %s: %w", moduleID, err)
return
}
results[idx] = rows
}(i, ref.id)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
var out []store.PrefixRow
for _, part := range results {
out = append(out, part...)
}
return out, nil
}
func rowsForModule(ctx context.Context, st store.Backend, hc *http.Client, tenantID string, mod *store.Module) ([]store.PrefixRow, error) {
if rows, ok, err := moduleRowsFromSnapshot(st, tenantID, mod); err != nil {
return nil, err
} else if ok {
return rows, nil
}
var prior []store.PrefixRow
if snap, ok, _ := st.GetModulePrefixSnapshot(tenantID, mod.ID); ok && snap != nil {
prior = snap.Prefixes
}
rows, err := collectModulePrefixRows(ctx, st, hc, tenantID, mod, prior)
if err != nil {
return nil, err
}
if err := persistModuleSnapshot(st, tenantID, mod, rows); err != nil {
return nil, err
}
return rows, nil
}
+58
View File
@@ -0,0 +1,58 @@
package pipeline
import (
"context"
"fmt"
"net/http"
"net/netip"
"os"
"strconv"
"strings"
"time"
"evobgp/internal/asnresolve"
"evobgp/internal/store"
)
func asnCacheTTL() time.Duration {
sec := 1800
if s := strings.TrimSpace(os.Getenv("EVOBGP_ASN_CACHE_TTL_SEC")); s != "" {
if v, err := strconv.Atoi(s); err == nil && v > 0 {
sec = v
}
}
return time.Duration(sec) * time.Second
}
// resolveASNForEntry fetches prefixes and holder with shared TTL cache (asn_prefix_cache).
func resolveASNForEntry(ctx context.Context, st store.Backend, hc *http.Client, asn int64) ([]netip.Prefix, string, error) {
ttl := asnCacheTTL()
if st != nil {
if ent, ok, err := st.GetASNPrefixCache(asn); err == nil && ok && ent != nil && time.Since(ent.FetchedAt) < ttl {
out := make([]netip.Prefix, 0, len(ent.Prefixes))
for _, p := range ent.Prefixes {
pfx, perr := netip.ParsePrefix(strings.TrimSpace(p))
if perr != nil {
continue
}
out = append(out, pfx.Masked())
}
return out, ent.Holder, nil
}
}
pfxs, err := asnresolve.AnnouncedPrefixes(ctx, hc, asn)
if err != nil {
return nil, "", err
}
holder, _ := asnresolve.ASHolderName(ctx, hc, asn)
if st != nil {
strs := make([]string, len(pfxs))
for i, p := range pfxs {
strs[i] = p.String()
}
if err := st.SetASNPrefixCache(asn, holder, strs); err != nil {
return nil, "", fmt.Errorf("asn cache AS%d: %w", asn, err)
}
}
return pfxs, holder, nil
}
+64
View File
@@ -0,0 +1,64 @@
package pipeline
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"evobgp/internal/store"
)
func TestResolveASNForEntry_UsesTTLCache(t *testing.T) {
m := store.NewMemory()
t.Setenv("EVOBGP_ASN_CACHE_TTL_SEC", "3600")
var calls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
if strings.Contains(r.URL.Path, "/announced") {
_, _ = w.Write([]byte(`{"status":"ok","data":{"prefixes":[{"prefix":"203.0.113.0/24"}]}}`))
return
}
_, _ = w.Write([]byte(`{"status":"ok","data":{"holder":"Test AS"}}`))
}))
defer srv.Close()
t.Setenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL", srv.URL+"/announced")
t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", srv.URL+"/overview")
ctx := context.Background()
hc := srv.Client()
p1, h1, err := resolveASNForEntry(ctx, m, hc, 64512)
if err != nil {
t.Fatal(err)
}
if len(p1) != 1 || h1 != "Test AS" {
t.Fatalf("unexpected first resolve: %+v holder=%q", p1, h1)
}
if calls.Load() < 2 {
t.Fatalf("expected ripestat calls on first resolve, got %d", calls.Load())
}
firstCalls := calls.Load()
_, _, err = resolveASNForEntry(ctx, m, hc, 64512)
if err != nil {
t.Fatal(err)
}
if calls.Load() != firstCalls {
t.Fatalf("expected cache hit (no new HTTP), calls went from %d to %d", firstCalls, calls.Load())
}
}
func TestModuleDueForScheduler_BucketRollover(t *testing.T) {
mod := &store.Module{Enabled: true, Type: "CDN_CIDRS", RefreshIntervalSec: 300}
boundary := time.Unix(300, 0)
if !ModuleDueForScheduler(mod, boundary) {
t.Fatal("expected due when refresh bucket rolls")
}
mid := time.Unix(330, 0)
if ModuleDueForScheduler(mod, mid) {
t.Fatal("expected not due within same bucket")
}
}
+149
View File
@@ -0,0 +1,149 @@
package pipeline
import (
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
"evobgp/internal/birdfmt"
"evobgp/internal/store"
)
type staticCommunityRoutes struct {
CommunityID string
RoutesV4 []birdfmt.StaticRoute
RoutesV6 []birdfmt.StaticRoute
}
// materializeRowsForBird turns prefix rows into export-filter CIDR lists, optional AS_PATH rules,
// and static routes grouped by community. The same prefix can appear in multiple community groups.
func materializeRowsForBird(st store.Backend, tenantID string, rows []store.PrefixRow) (
filterV4, filterV6 []netip.Prefix,
pathASNs []int64,
staticByCommunity []staticCommunityRoutes,
err error,
) {
var pathSeen map[int64]struct{}
addPath := func(n int64) {
if pathSeen == nil {
pathSeen = make(map[int64]struct{})
}
if _, ok := pathSeen[n]; ok {
return
}
pathSeen[n] = struct{}{}
pathASNs = append(pathASNs, n)
}
filterSeen := make(map[string]struct{})
type staticGroup struct {
v4 map[string]birdfmt.StaticRoute
v6 map[string]birdfmt.StaticRoute
}
staticGroups := make(map[string]*staticGroup)
routeBodyByCommunity := make(map[string]string)
for _, pr := range rows {
p := strings.TrimSpace(pr.Prefix)
if strings.HasPrefix(p, "as:") {
n, err := strconv.ParseInt(strings.TrimPrefix(p, "as:"), 10, 64)
if err != nil || !store.ValidASN(n) {
continue
}
addPath(n)
continue
}
pfx, err := netip.ParsePrefix(p)
if err != nil {
continue
}
pfx = pfx.Masked()
key := pfx.String()
if _, ok := filterSeen[key]; !ok {
filterSeen[key] = struct{}{}
if pfx.Addr().Is4() {
filterV4 = append(filterV4, pfx)
} else {
filterV6 = append(filterV6, pfx)
}
}
communityID := ""
if pr.CommunityID != nil {
communityID = strings.TrimSpace(*pr.CommunityID)
}
body, ok := routeBodyByCommunity[communityID]
if !ok {
body, err = communityRouteBody(st, tenantID, pr.CommunityID)
if err != nil {
return nil, nil, nil, nil, err
}
routeBodyByCommunity[communityID] = body
}
grp, ok := staticGroups[communityID]
if !ok {
grp = &staticGroup{
v4: make(map[string]birdfmt.StaticRoute),
v6: make(map[string]birdfmt.StaticRoute),
}
staticGroups[communityID] = grp
}
if pfx.Addr().Is4() {
if _, exists := grp.v4[key]; !exists {
grp.v4[key] = birdfmt.StaticRoute{Prefix: pfx, RouteBody: body}
}
} else if pfx.Addr().Is6() {
if _, exists := grp.v6[key]; !exists {
grp.v6[key] = birdfmt.StaticRoute{Prefix: pfx, RouteBody: body}
}
}
}
groupKeys := make([]string, 0, len(staticGroups))
for k := range staticGroups {
groupKeys = append(groupKeys, k)
}
sort.Strings(groupKeys)
for _, gid := range groupKeys {
grp := staticGroups[gid]
out := staticCommunityRoutes{CommunityID: gid}
v4Keys := make([]string, 0, len(grp.v4))
for k := range grp.v4 {
v4Keys = append(v4Keys, k)
}
sort.Strings(v4Keys)
for _, k := range v4Keys {
out.RoutesV4 = append(out.RoutesV4, grp.v4[k])
}
v6Keys := make([]string, 0, len(grp.v6))
for k := range grp.v6 {
v6Keys = append(v6Keys, k)
}
sort.Strings(v6Keys)
for _, k := range v6Keys {
out.RoutesV6 = append(out.RoutesV6, grp.v6[k])
}
staticByCommunity = append(staticByCommunity, out)
}
return filterV4, filterV6, pathASNs, staticByCommunity, nil
}
func communityRouteBody(st store.Backend, tenantID string, cid *string) (string, error) {
if cid == nil {
return "", nil
}
id := strings.TrimSpace(*cid)
if id == "" {
return "", nil
}
c, err := st.GetCommunity(tenantID, id)
if err != nil {
return "", fmt.Errorf("community %s: %w", id, err)
}
return birdfmt.RouteCommunityAttrs("", c.Community, c.ValueJSON)
}
+192
View File
@@ -0,0 +1,192 @@
package pipeline
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"evobgp/internal/store"
)
func TestCollectModulePrefixRows_CDNSendsIfNoneMatch(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, _, _ := m.DemoIDs()
mod, err := m.CreateModule(tenant, &store.Module{
Type: "CDN_CIDRS",
Name: "cdn-conditional",
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
var gotIfNoneMatch string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotIfNoneMatch = strings.TrimSpace(r.Header.Get("If-None-Match"))
w.Header().Set("ETag", "etag-new")
_, _ = w.Write([]byte("198.51.100.0/24\n"))
}))
defer srv.Close()
etag := "etag-old"
if _, err := m.CreateCDNSource(tenant, mod.ID, &store.CDNSource{
SourceKind: "txt",
URL: srv.URL,
Etag: etag,
}); err != nil {
t.Fatal(err)
}
collected, err := collectModulePrefixRows(context.Background(), m, srv.Client(), tenant, mod, nil)
if err != nil {
t.Fatal(err)
}
if gotIfNoneMatch != etag {
t.Fatalf("want If-None-Match %q, got %q", etag, gotIfNoneMatch)
}
if len(collected) != 1 || collected[0].Prefix != "198.51.100.0/24" {
t.Fatalf("unexpected collected rows: %+v", collected)
}
}
func TestCollectModulePrefixRows_CDN304UsesSnapshot(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, _, _ := m.DemoIDs()
mod, err := m.CreateModule(tenant, &store.Module{
Type: "CDN_CIDRS",
Name: "cdn-304",
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotModified)
}))
defer srv.Close()
etag := "etag-stable"
src, err := m.CreateCDNSource(tenant, mod.ID, &store.CDNSource{
SourceKind: "txt",
URL: srv.URL,
Etag: etag,
})
if err != nil {
t.Fatal(err)
}
prior := []store.PrefixRow{{
Prefix: "203.0.113.0/24",
Source: cdnSourceKey(src.ID),
}}
if err := mergeCDNSourceIntoModuleSnapshot(m, tenant, mod, src.ID, prior); err != nil {
t.Fatal(err)
}
collected, err := collectModulePrefixRows(context.Background(), m, srv.Client(), tenant, mod, prior)
if err != nil {
t.Fatal(err)
}
if len(collected) != 1 || collected[0].Prefix != "203.0.113.0/24" {
t.Fatalf("want cached prefix on 304, got %+v", collected)
}
}
func TestRefreshModuleIngest_CDN304UsesStoredSnapshot(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, _, _ := m.DemoIDs()
mod, err := m.CreateModule(tenant, &store.Module{
Type: "CDN_CIDRS",
Name: "cdn-304-ingest",
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotModified)
}))
defer srv.Close()
etag := "etag-stable"
src, err := m.CreateCDNSource(tenant, mod.ID, &store.CDNSource{
SourceKind: "txt",
URL: srv.URL,
Etag: etag,
})
if err != nil {
t.Fatal(err)
}
prior := []store.PrefixRow{{
Prefix: "203.0.113.0/24",
Source: cdnSourceKey(src.ID),
}}
if err := mergeCDNSourceIntoModuleSnapshot(m, tenant, mod, src.ID, prior); err != nil {
t.Fatal(err)
}
if err := RefreshModuleIngest(context.Background(), m, srv.Client(), tenant, mod.ID); err != nil {
t.Fatal(err)
}
}
func TestCollectModulePrefixRows_CDN304RetriesWithoutETag(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, _, _ := m.DemoIDs()
mod, err := m.CreateModule(tenant, &store.Module{
Type: "CDN_CIDRS",
Name: "cdn-304-retry",
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if calls == 1 {
if got := strings.TrimSpace(r.Header.Get("If-None-Match")); got != "etag-stable" {
t.Fatalf("first request want If-None-Match etag-stable, got %q", got)
}
w.WriteHeader(http.StatusNotModified)
return
}
if got := strings.TrimSpace(r.Header.Get("If-None-Match")); got != "" {
t.Fatalf("retry must omit If-None-Match, got %q", got)
}
w.Header().Set("ETag", "etag-stable")
_, _ = w.Write([]byte("198.51.100.0/24\n"))
}))
defer srv.Close()
if _, err := m.CreateCDNSource(tenant, mod.ID, &store.CDNSource{
SourceKind: "txt",
URL: srv.URL,
Etag: "etag-stable",
}); err != nil {
t.Fatal(err)
}
collected, err := collectModulePrefixRows(context.Background(), m, srv.Client(), tenant, mod, nil)
if err != nil {
t.Fatal(err)
}
if calls != 2 {
t.Fatalf("want 2 HTTP calls (304 then 200), got %d", calls)
}
if len(collected) != 1 || collected[0].Prefix != "198.51.100.0/24" {
t.Fatalf("unexpected collected rows: %+v", collected)
}
}
+158
View File
@@ -0,0 +1,158 @@
package pipeline
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"evobgp/internal/store"
)
func cdnSourceKey(sourceID string) string {
return "cdn:" + strings.TrimSpace(sourceID)
}
func cachedCDNPrefixRows(st store.Backend, tenantID, moduleID string, priorSnapshot []store.PrefixRow, sourceKey string) []store.PrefixRow {
if cached := prefixRowsForSource(priorSnapshot, sourceKey); len(cached) > 0 {
return cached
}
if st != nil {
if snap, ok, _ := st.GetModulePrefixSnapshot(tenantID, moduleID); ok && snap != nil {
if cached := prefixRowsForSource(snap.Prefixes, sourceKey); len(cached) > 0 {
return cached
}
}
if cached := latestCDNRowsBySource(st, tenantID)[sourceKey]; len(cached) > 0 {
return cached
}
}
return nil
}
func mergeSnapshotDropSource(rows []store.PrefixRow, sourceKey string) []store.PrefixRow {
if len(rows) == 0 {
return nil
}
out := make([]store.PrefixRow, 0, len(rows))
for _, row := range rows {
if row.Source != sourceKey {
out = append(out, row)
}
}
return out
}
func cdnRowsFromParsed(mod *store.Module, src *store.CDNSource, pfxStrings []string) []store.PrefixRow {
var rows []store.PrefixRow
for _, p := range pfxStrings {
p = strings.TrimSpace(p)
if p == "" {
continue
}
comm := src.CommunityID
if comm == nil && mod != nil && mod.DefaultCommunityID != nil {
c := *mod.DefaultCommunityID
comm = &c
}
rows = append(rows, store.PrefixRow{Prefix: p, CommunityID: comm, Source: cdnSourceKey(src.ID)})
}
return rows
}
// mergeCDNSourceIntoModuleSnapshot replaces rows for one CDN source in the module snapshot.
func mergeCDNSourceIntoModuleSnapshot(st store.Backend, tenantID string, mod *store.Module, sourceID string, newRows []store.PrefixRow) error {
if st == nil || mod == nil {
return nil
}
sourceKey := cdnSourceKey(sourceID)
var base []store.PrefixRow
if snap, ok, _ := st.GetModulePrefixSnapshot(tenantID, mod.ID); ok && snap != nil {
base = mergeSnapshotDropSource(snap.Prefixes, sourceKey)
}
merged := append(base, newRows...)
return persistModuleSnapshot(st, tenantID, mod, merged)
}
func parseCDNBody(body string, src *store.CDNSource) ([]string, error) {
pfxs, err := ExtractCIDRs(body, src.SourceKind, src.PrefixPath)
if err != nil {
return nil, err
}
out := make([]string, 0, len(pfxs))
for _, pfx := range pfxs {
out = append(out, pfx.String())
}
return out, nil
}
func applyCDNSourceHTTPResult(ctx context.Context, st store.Backend, hc *http.Client, tenantID, moduleID string, mod *store.Module, src *store.CDNSource, priorSnapshot []store.PrefixRow, now time.Time) ([]store.PrefixRow, error) {
u := strings.TrimSpace(src.URL)
if u == "" {
return nil, nil
}
sourceKey := cdnSourceKey(src.ID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
if etag := strings.TrimSpace(src.Etag); etag != "" {
req.Header.Set("If-None-Match", etag)
}
resp, err := hc.Do(req)
if err != nil {
return nil, fmt.Errorf("cdn fetch %s: %w", u, err)
}
if resp.StatusCode == http.StatusNotModified {
if cached := cachedCDNPrefixRows(st, tenantID, moduleID, priorSnapshot, sourceKey); len(cached) > 0 {
_ = resp.Body.Close()
return cached, nil
}
// ETag is known but local snapshot is empty — force a full download.
_ = resp.Body.Close()
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, err
}
resp, err = hc.Do(req2)
if err != nil {
return nil, fmt.Errorf("cdn fetch %s: %w", u, err)
}
if resp.StatusCode == http.StatusNotModified {
_ = resp.Body.Close()
return nil, fmt.Errorf("cdn url %s: 304 without cached prefixes", u)
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
return nil, fmt.Errorf("cdn url %s: %s", u, resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
return nil, err
}
prefixStrs, err := parseCDNBody(string(body), src)
if err != nil {
return nil, fmt.Errorf("cdn parse %s: %w", u, err)
}
etag := strings.TrimSpace(resp.Header.Get("ETag"))
patch := &store.CDNSourcePatch{}
if etag != "" && etag != strings.TrimSpace(src.Etag) {
e := etag
patch.Etag = &e
}
refreshedAt := now
patch.LastRefreshedAt = &refreshedAt
_, _ = st.UpdateCDNSource(tenantID, moduleID, src.ID, patch)
rows := cdnRowsFromParsed(mod, src, prefixStrs)
if err := mergeCDNSourceIntoModuleSnapshot(st, tenantID, mod, src.ID, rows); err != nil {
return nil, err
}
return rows, nil
}
+244
View File
@@ -0,0 +1,244 @@
package pipeline
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
"evobgp/internal/store"
)
func prefixRowsForSource(rows []store.PrefixRow, sourceKey string) []store.PrefixRow {
if len(rows) == 0 {
return nil
}
var out []store.PrefixRow
for _, row := range rows {
if row.Source == sourceKey {
out = append(out, row)
}
}
return out
}
func collectASPrefixRows(ctx context.Context, st store.Backend, hc *http.Client, tenantID string, mod *store.Module, list []*store.ASEntry) ([]store.PrefixRow, error) {
moduleID := mod.ID
legacy := strings.TrimSpace(os.Getenv("EVOBGP_ASN_RESOLVE")) == "0"
if legacy {
var rows []store.PrefixRow
for _, e := range list {
if !store.ValidASN(e.ASN) {
continue
}
comm := e.CommunityID
if comm == nil && mod.DefaultCommunityID != nil {
c := *mod.DefaultCommunityID
comm = &c
}
rows = append(rows, store.PrefixRow{Prefix: MaterializedASPrefixKey(e.ASN), CommunityID: comm, Source: "as_entry"})
}
return rows, nil
}
type entryResult struct {
rows []store.PrefixRow
metaID string
asn int64
holder string
count int64
err error
}
var valid []*store.ASEntry
for _, e := range list {
if e != nil && store.ValidASN(e.ASN) {
valid = append(valid, e)
}
}
sem := make(chan struct{}, collectConcurrency())
results := make([]entryResult, len(valid))
var wg sync.WaitGroup
for i, e := range valid {
wg.Add(1)
go func(idx int, entry *store.ASEntry) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
comm := entry.CommunityID
if comm == nil && mod.DefaultCommunityID != nil {
c := *mod.DefaultCommunityID
comm = &c
}
pfxs, holder, err := resolveASNForEntry(ctx, st, hc, entry.ASN)
if err != nil {
results[idx] = entryResult{err: fmt.Errorf("resolve AS%d: %w", entry.ASN, err)}
return
}
src := fmt.Sprintf("as:%d", entry.ASN)
var rows []store.PrefixRow
for _, pfx := range pfxs {
rows = append(rows, store.PrefixRow{Prefix: pfx.String(), CommunityID: comm, Source: src})
}
results[idx] = entryResult{
rows: rows,
metaID: entry.ID,
asn: entry.ASN,
holder: holder,
count: int64(len(pfxs)),
}
}(i, e)
}
wg.Wait()
seenPfx := make(map[string]struct{})
var out []store.PrefixRow
now := time.Now().UTC()
for _, r := range results {
if r.err != nil {
return nil, r.err
}
if r.metaID != "" {
if err := st.UpdateASEntryResolveMeta(tenantID, moduleID, r.metaID, r.holder, r.count, now); err != nil {
return nil, fmt.Errorf("as entry meta AS%d: %w", r.asn, err)
}
}
for _, row := range r.rows {
k := row.Prefix
if _, ok := seenPfx[k]; ok {
continue
}
seenPfx[k] = struct{}{}
out = append(out, row)
}
}
return out, nil
}
func collectCDNPrefixRows(ctx context.Context, st store.Backend, hc *http.Client, tenantID string, mod *store.Module, sources []*store.CDNSource, priorSnapshot []store.PrefixRow) ([]store.PrefixRow, error) {
moduleID := mod.ID
now := time.Now().UTC()
var valid []*store.CDNSource
for _, s := range sources {
if s != nil {
valid = append(valid, s)
}
}
type srcResult struct {
rows []store.PrefixRow
err error
}
results := make([]srcResult, len(valid))
sem := make(chan struct{}, collectConcurrency())
var wg sync.WaitGroup
for i, src := range valid {
wg.Add(1)
go func(idx int, src *store.CDNSource) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
sourceKey := "cdn:" + src.ID
if shouldSkipCDNSourceFetch(src, now) {
if cached := prefixRowsForSource(priorSnapshot, sourceKey); len(cached) > 0 {
results[idx] = srcResult{rows: cached}
return
}
if cached := latestCDNRowsBySource(st, tenantID)[sourceKey]; len(cached) > 0 {
results[idx] = srcResult{rows: cached}
return
}
}
rows, err := applyCDNSourceHTTPResult(ctx, st, hc, tenantID, moduleID, mod, src, priorSnapshot, now)
if err != nil {
results[idx] = srcResult{err: err}
return
}
results[idx] = srcResult{rows: rows}
}(i, src)
}
wg.Wait()
var out []store.PrefixRow
for _, r := range results {
if r.err != nil {
return nil, r.err
}
out = append(out, r.rows...)
}
return out, nil
}
func collectDomainPrefixRows(ctx context.Context, hc *http.Client, mod *store.Module, profiles []*store.DohProfile, policy string, entries []*store.DomainEntry) ([]store.PrefixRow, error) {
var validDom []*store.DomainEntry
for _, e := range entries {
if e != nil {
validDom = append(validDom, e)
}
}
type domResult struct {
rows []store.PrefixRow
err error
}
results := make([]domResult, len(validDom))
sem := make(chan struct{}, collectConcurrency())
var wg sync.WaitGroup
for i, e := range validDom {
wg.Add(1)
go func(idx int, entry *store.DomainEntry) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
comm := entry.CommunityID
if comm == nil && mod.DefaultCommunityID != nil {
c := *mod.DefaultCommunityID
comm = &c
}
addrs, err := resolveDomainIPsWithPolicy(ctx, hc, profiles, policy, entry.FQDN)
if err != nil {
results[idx] = domResult{err: fmt.Errorf("resolve domain %q: %w", entry.FQDN, err)}
return
}
src := "domain:" + strings.TrimSpace(entry.FQDN)
var rows []store.PrefixRow
for _, ip := range addrs {
cidr := ipToHostPrefix(ip)
if cidr == "" {
continue
}
rows = append(rows, store.PrefixRow{
Prefix: cidr,
CommunityID: comm,
Source: src,
})
}
results[idx] = domResult{rows: rows}
}(i, e)
}
wg.Wait()
seen := make(map[string]struct{})
var out []store.PrefixRow
for _, r := range results {
if r.err != nil {
return nil, r.err
}
for _, row := range r.rows {
key := row.Prefix + "|" + row.Source
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, row)
}
}
return out, nil
}
+3
View File
@@ -0,0 +1,3 @@
// Package pipeline ingests module inputs (CDN, ASN, domains, IP ranges), materializes
// prefixes, and creates BIRD preview revisions via store.
package pipeline
+131
View File
@@ -0,0 +1,131 @@
package pipeline
import (
"context"
"fmt"
"net/http"
"net/netip"
"strings"
"time"
"evobgp/internal/store"
"github.com/miekg/dns"
)
func loadModuleDohProfiles(st store.Backend, tenantID string, mod *store.Module) ([]*store.DohProfile, string, error) {
if mod == nil {
return nil, store.DohPolicyPrimaryOnly, nil
}
policy := store.NormalizeDohResolverPolicy(mod.DohResolverPolicy)
var profiles []*store.DohProfile
for _, id := range mod.EffectiveDohProfileIDs() {
prof, err := st.GetDohProfile(tenantID, id)
if err != nil {
return nil, "", fmt.Errorf("get doh profile %s: %w", id, err)
}
profiles = append(profiles, prof)
}
return profiles, policy, nil
}
func resolveDomainIPsWithPolicy(ctx context.Context, hc *http.Client, profiles []*store.DohProfile, policy, fqdn string) ([]netip.Addr, error) {
policy = store.NormalizeDohResolverPolicy(policy)
if len(profiles) == 0 {
return resolveDomainIPs(ctx, hc, nil, fqdn)
}
if len(profiles) == 1 {
return resolveDomainIPs(ctx, hc, profiles[0], fqdn)
}
switch policy {
case store.DohPolicyUnion:
return resolveDomainIPsUnion(ctx, hc, profiles, fqdn)
case store.DohPolicyFailover:
return resolveDomainIPsFailover(ctx, hc, profiles, fqdn)
default:
return resolveDomainIPs(ctx, hc, profiles[0], fqdn)
}
}
func resolveDomainIPsUnion(ctx context.Context, hc *http.Client, profiles []*store.DohProfile, fqdn string) ([]netip.Addr, error) {
var merged []netip.Addr
var errs []error
for _, prof := range profiles {
if prof == nil {
continue
}
ips, err := resolveDomainIPsNoSystemFallback(ctx, hc, prof, fqdn)
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", strings.TrimSpace(prof.URL), err))
continue
}
merged = append(merged, ips...)
}
merged = uniqAddrs(merged)
if len(merged) > 0 {
return merged, nil
}
if len(errs) > 0 {
return nil, fmt.Errorf("doh union failed: %v", errs)
}
return nil, nil
}
func resolveDomainIPsFailover(ctx context.Context, hc *http.Client, profiles []*store.DohProfile, fqdn string) ([]netip.Addr, error) {
var lastErr error
for _, prof := range profiles {
if prof == nil {
continue
}
ips, err := resolveDomainIPsNoSystemFallback(ctx, hc, prof, fqdn)
if err != nil {
lastErr = err
continue
}
if len(ips) > 0 {
return ips, nil
}
}
if lastErr != nil {
return nil, lastErr
}
return resolveDomainIPs(ctx, hc, nil, fqdn)
}
// resolveDomainIPsNoSystemFallback queries one DoH profile without falling back to OS resolver.
func resolveDomainIPsNoSystemFallback(ctx context.Context, hc *http.Client, profile *store.DohProfile, fqdn string) ([]netip.Addr, error) {
host := strings.TrimSpace(strings.TrimSuffix(fqdn, "."))
if host == "" {
return nil, nil
}
if profile == nil || strings.TrimSpace(profile.URL) == "" {
return nil, fmt.Errorf("empty doh profile")
}
timeout := dohProfileTimeout(profile)
dctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
baseURL := strings.TrimSpace(profile.URL)
v4, err4 := resolveDomainWithDOHMessage(dctx, hc, baseURL, host, dns.TypeA)
v6, err6 := resolveDomainWithDOHMessage(dctx, hc, baseURL, host, dns.TypeAAAA)
if err4 != nil {
v4, err4 = resolveDomainWithDOHJSON(dctx, hc, baseURL, host, "A")
}
if err6 != nil {
v6, err6 = resolveDomainWithDOHJSON(dctx, hc, baseURL, host, "AAAA")
}
if err4 != nil && err6 != nil {
return nil, fmt.Errorf("doh failed for A and AAAA: %v; %v", err4, err6)
}
return uniqAddrs(append(v4, v6...)), nil
}
func dohProfileTimeout(profile *store.DohProfile) time.Duration {
timeout := 10 * time.Second
if profile != nil && profile.TimeoutMs != nil && *profile.TimeoutMs > 0 {
timeout = time.Duration(*profile.TimeoutMs) * time.Millisecond
}
return timeout
}
+148
View File
@@ -0,0 +1,148 @@
package pipeline
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"evobgp/internal/store"
)
func TestResolveDomainIPsWithPolicy_Union(t *testing.T) {
srvRU := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"198.51.100.1"}]}`))
}))
defer srvRU.Close()
srvEU := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"203.0.113.1"}]}`))
}))
defer srvEU.Close()
profiles := []*store.DohProfile{
{URL: srvRU.URL},
{URL: srvEU.URL},
}
ips, err := resolveDomainIPsWithPolicy(context.Background(), srvRU.Client(), profiles, store.DohPolicyUnion, "example.com")
if err != nil {
t.Fatal(err)
}
if len(ips) != 2 {
t.Fatalf("want 2 ips, got %v", ips)
}
seen := map[string]bool{ips[0].String(): true, ips[1].String(): true}
if !seen["198.51.100.1"] || !seen["203.0.113.1"] {
t.Fatalf("unexpected ips: %v", ips)
}
}
func TestResolveDomainIPsWithPolicy_Failover(t *testing.T) {
var calls int
srvBad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
http.Error(w, "fail", http.StatusBadGateway)
}))
defer srvBad.Close()
srvOK := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"198.51.100.5"}]}`))
}))
defer srvOK.Close()
profiles := []*store.DohProfile{
{URL: srvBad.URL},
{URL: srvOK.URL},
}
ips, err := resolveDomainIPsWithPolicy(context.Background(), srvBad.Client(), profiles, store.DohPolicyFailover, "example.com")
if err != nil {
t.Fatal(err)
}
if len(ips) != 1 || ips[0].String() != "198.51.100.5" {
t.Fatalf("unexpected ips: %v", ips)
}
if calls < 2 {
t.Fatalf("want at least 2 resolver calls, got %d", calls)
}
}
func TestResolveDomainIPsWithPolicy_PrimaryOnly(t *testing.T) {
var secondCalled bool
srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"198.51.100.9"}]}`))
}))
defer srv1.Close()
srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
secondCalled = true
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"203.0.113.9"}]}`))
}))
defer srv2.Close()
profiles := []*store.DohProfile{
{URL: srv1.URL},
{URL: srv2.URL},
}
ips, err := resolveDomainIPsWithPolicy(context.Background(), srv1.Client(), profiles, store.DohPolicyPrimaryOnly, "example.com")
if err != nil {
t.Fatal(err)
}
if len(ips) != 1 || ips[0].String() != "198.51.100.9" {
t.Fatalf("unexpected ips: %v", ips)
}
if secondCalled {
t.Fatal("secondary resolver must not be queried in primary_only mode")
}
}
func TestCollectModulePrefixRows_DohUnion(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, _, _ := m.DemoIDs()
mod, err := m.CreateModule(tenant, &store.Module{
Type: "DOMAINS",
Name: "domains-union",
Enabled: true,
DohResolverPolicy: store.DohPolicyUnion,
})
if err != nil {
t.Fatal(err)
}
srvRU := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"198.51.100.2"}]}`))
}))
defer srvRU.Close()
srvEU := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"Answer":[{"type":1,"data":"203.0.113.2"}]}`))
}))
defer srvEU.Close()
ru, err := m.CreateDohProfile(tenant, &store.DohProfile{Name: "ru", URL: srvRU.URL})
if err != nil {
t.Fatal(err)
}
eu, err := m.CreateDohProfile(tenant, &store.DohProfile{Name: "eu", URL: srvEU.URL})
if err != nil {
t.Fatal(err)
}
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{
DohProfileIDs: &[]string{ru.ID, eu.ID},
}); err != nil {
t.Fatal(err)
}
mod, err = m.GetModule(tenant, mod.ID)
if err != nil {
t.Fatal(err)
}
if _, err := m.CreateDomainEntry(tenant, mod.ID, &store.DomainEntry{FQDN: "svc.example.com"}); err != nil {
t.Fatal(err)
}
rows, err := collectModulePrefixRows(context.Background(), m, srvRU.Client(), tenant, mod, nil)
if err != nil {
t.Fatal(err)
}
if len(rows) != 2 {
t.Fatalf("want 2 prefix rows, got %+v", rows)
}
}
@@ -0,0 +1,49 @@
package pipeline
import (
"strings"
"testing"
"evobgp/internal/birdfmt"
"evobgp/internal/store"
)
func TestBuildPreviewFragments_SamePrefixDifferentCommunity(t *testing.T) {
m := store.NewMemory()
m.SeedDemo()
tenant, _, moduleIP, _, _ := m.DemoIDs()
c1, err := m.CreateCommunity(tenant, &store.Community{Community: "65000:1", Title: "A", ValueJSON: "{}"})
if err != nil {
t.Fatal(err)
}
c2, err := m.CreateCommunity(tenant, &store.Community{Community: "65000:2", Title: "B", ValueJSON: "{}"})
if err != nil {
t.Fatal(err)
}
rows := []store.PrefixRow{
{Prefix: "203.0.113.0/24", CommunityID: &c1.ID, Source: "ip_range"},
{Prefix: "203.0.113.0/24", CommunityID: &c2.ID, Source: "cdn:test"},
}
out, err := buildPreviewFragments(m, tenant, moduleIP, "test-revision", rows)
if err != nil {
t.Fatal(err)
}
staticV4 := out[birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV4)]
if !strings.Contains(staticV4, "protocol static evobgp_prefixes_v4_"+communityProtocolSuffix(c1.ID)) {
t.Fatalf("expected dedicated static protocol for first community, got:\n%s", staticV4)
}
if !strings.Contains(staticV4, "protocol static evobgp_prefixes_v4_"+communityProtocolSuffix(c2.ID)) {
t.Fatalf("expected dedicated static protocol for second community, got:\n%s", staticV4)
}
out2, err := buildPreviewFragments(m, tenant, moduleIP, "test-revision", rows)
if err != nil {
t.Fatal(err)
}
staticV4Second := out2[birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV4)]
if staticV4 != staticV4Second {
t.Fatalf("expected deterministic static preview text, got first:\n%s\nsecond:\n%s", staticV4, staticV4Second)
}
}
+122
View File
@@ -0,0 +1,122 @@
package pipeline
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
"evobgp/internal/store"
)
// moduleIngestInputHash fingerprints module config and child entries so snapshots invalidate on CRUD.
func moduleIngestInputHash(st store.Backend, tenantID string, mod *store.Module) (string, error) {
if st == nil || mod == nil {
return "", fmt.Errorf("pipeline: module hash: missing store or module")
}
h := sha256.New()
_, _ = fmt.Fprintf(h, "type=%s\n", strings.TrimSpace(mod.Type))
_, _ = fmt.Fprintf(h, "enabled=%t\n", mod.Enabled)
if mod.DefaultCommunityID != nil {
_, _ = fmt.Fprintf(h, "default_community=%s\n", strings.TrimSpace(*mod.DefaultCommunityID))
}
_, _ = fmt.Fprintf(h, "doh_policy=%s\n", store.NormalizeDohResolverPolicy(mod.DohResolverPolicy))
for _, pid := range mod.EffectiveDohProfileIDs() {
_, _ = fmt.Fprintf(h, "doh_profile=%s\n", pid)
if prof, err := st.GetDohProfile(tenantID, pid); err == nil && prof != nil {
_, _ = fmt.Fprintf(h, "doh_url=%s\n", strings.TrimSpace(prof.URL))
if prof.TimeoutMs != nil {
_, _ = fmt.Fprintf(h, "doh_timeout=%d\n", *prof.TimeoutMs)
}
}
}
switch mod.Type {
case "IP_RANGES":
list, err := st.ListIPRangeEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].Prefix < list[j].Prefix })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "ip=%s|c=%s\n", e.Prefix, comm)
}
case "AS_PREFIXES":
list, err := st.ListASEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].ASN < list[j].ASN })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "as=%d|c=%s\n", e.ASN, comm)
}
case "CDN_CIDRS":
list, err := st.ListCDNSources(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
for _, s := range list {
comm := ""
if s.CommunityID != nil {
comm = *s.CommunityID
}
interval := 0
if s.RefreshIntervalSec != nil {
interval = *s.RefreshIntervalSec
}
_, _ = fmt.Fprintf(h, "cdn=%s|url=%s|kind=%s|path=%s|c=%s|etag=%s|interval=%d\n",
s.ID, strings.TrimSpace(s.URL), s.SourceKind, strings.TrimSpace(s.PrefixPath), comm,
strings.TrimSpace(s.Etag), interval)
}
case "DOMAINS":
list, err := st.ListDomainEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].FQDN < list[j].FQDN })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "dom=%s|c=%s\n", strings.TrimSpace(e.FQDN), comm)
}
default:
_, _ = fmt.Fprintf(h, "unknown_type=%s\n", mod.Type)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func persistModuleSnapshot(st store.Backend, tenantID string, mod *store.Module, rows []store.PrefixRow) error {
if st == nil || mod == nil {
return nil
}
hash, err := moduleIngestInputHash(st, tenantID, mod)
if err != nil {
return err
}
return st.SetModulePrefixSnapshot(tenantID, mod.ID, hash, rows)
}
func moduleRowsFromSnapshot(st store.Backend, tenantID string, mod *store.Module) ([]store.PrefixRow, bool, error) {
hash, err := moduleIngestInputHash(st, tenantID, mod)
if err != nil {
return nil, false, err
}
snap, ok, err := st.GetModulePrefixSnapshot(tenantID, mod.ID)
if err != nil || !ok || snap == nil || snap.InputHash != hash {
return nil, false, err
}
cp := append([]store.PrefixRow(nil), snap.Prefixes...)
return cp, true, nil
}
+128
View File
@@ -2,10 +2,18 @@ package pipeline
import (
"bufio"
"encoding/json"
"fmt"
"net/netip"
"strings"
)
// Защита от pathological JSON: глубокая рекурсия при обходе и взрыв числа узлов по пути.
const (
maxJSONWalkDepth = 512
maxJSONPathBreadth = 50000
)
// ParseCIDRLines extracts unique IPv4/IPv6 CIDRs from plain text (one per line, # comments, empty lines skipped).
func ParseCIDRLines(body string) []netip.Prefix {
seen := make(map[string]struct{})
@@ -31,6 +39,126 @@ func ParseCIDRLines(body string) []netip.Prefix {
return out
}
// ExtractCIDRs parses CIDRs from either plaintext lines or JSON payload.
// For sourceKind="json", prefixPath supports dotted traversal, with [] for arrays:
// e.g. "prefixes[]", "data.items[].cidr".
func ExtractCIDRs(body, sourceKind, prefixPath string) ([]netip.Prefix, error) {
if strings.EqualFold(strings.TrimSpace(sourceKind), "json") {
return parseCIDRsFromJSON(body, prefixPath)
}
return ParseCIDRLines(body), nil
}
func parseCIDRsFromJSON(body, prefixPath string) ([]netip.Prefix, error) {
var root any
if err := json.Unmarshal([]byte(body), &root); err != nil {
return nil, err
}
values, err := jsonValuesAtPath(root, prefixPath)
if err != nil {
return nil, err
}
seen := make(map[string]struct{})
var out []netip.Prefix
for _, raw := range values {
pfx := parseOneCIDR(raw)
if !pfx.IsValid() {
continue
}
m := pfx.Masked()
s := m.String()
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
out = append(out, m)
}
return out, nil
}
func jsonValuesAtPath(root any, prefixPath string) ([]string, error) {
path := strings.TrimSpace(prefixPath)
if path == "" {
return flattenJSONStrings(root, 0)
}
parts := strings.Split(path, ".")
nodes := []any{root}
for _, p := range parts {
part := strings.TrimSpace(p)
if part == "" {
continue
}
iter := strings.HasSuffix(part, "[]")
key := strings.TrimSuffix(part, "[]")
var next []any
for _, n := range nodes {
obj, ok := n.(map[string]any)
if !ok {
continue
}
child, ok := obj[key]
if !ok {
continue
}
if iter {
if arr, ok := child.([]any); ok {
next = append(next, arr...)
}
continue
}
next = append(next, child)
}
if len(next) > maxJSONPathBreadth {
return nil, fmt.Errorf("json path: слишком много узлов на шаге (>%d)", maxJSONPathBreadth)
}
nodes = next
if len(nodes) == 0 {
return nil, nil
}
}
var out []string
for _, n := range nodes {
part, err := flattenJSONStrings(n, 0)
if err != nil {
return nil, err
}
out = append(out, part...)
}
return out, nil
}
func flattenJSONStrings(v any, depth int) ([]string, error) {
if depth > maxJSONWalkDepth {
return nil, fmt.Errorf("json: глубина вложенности превышает %d", maxJSONWalkDepth)
}
switch x := v.(type) {
case string:
return []string{strings.TrimSpace(x)}, nil
case []any:
var out []string
for _, item := range x {
part, err := flattenJSONStrings(item, depth+1)
if err != nil {
return nil, err
}
out = append(out, part...)
}
return out, nil
case map[string]any:
var out []string
for _, item := range x {
part, err := flattenJSONStrings(item, depth+1)
if err != nil {
return nil, err
}
out = append(out, part...)
}
return out, nil
default:
return nil, nil
}
}
func parseOneCIDR(s string) netip.Prefix {
if p, err := netip.ParsePrefix(s); err == nil {
return p
+50
View File
@@ -0,0 +1,50 @@
package pipeline
import (
"strings"
"testing"
)
func TestExtractCIDRs_JSONNested(t *testing.T) {
body := `{"a":{"b":{"c":"192.0.2.0/24"}}}`
pfx, err := ExtractCIDRs(body, "json", "a.b.c")
if err != nil {
t.Fatal(err)
}
if len(pfx) != 1 || pfx[0].String() != "192.0.2.0/24" {
t.Fatalf("got %#v", pfx)
}
}
func TestExtractCIDRs_JSONDepthLimit(t *testing.T) {
var b strings.Builder
b.WriteString(`{"x":`)
for i := 0; i < maxJSONWalkDepth+4; i++ {
b.WriteString(`{"k":`)
}
b.WriteString(`"192.0.2.1"`)
for i := 0; i < maxJSONWalkDepth+4; i++ {
b.WriteByte('}')
}
b.WriteByte('}')
_, err := ExtractCIDRs(b.String(), "json", "")
if err == nil {
t.Fatal("expected depth error")
}
}
func TestExtractCIDRs_JSONPathBreadth(t *testing.T) {
var b strings.Builder
b.WriteString(`{"items":{"x":[`)
for i := 0; i < maxJSONPathBreadth+1; i++ {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(`"192.0.2.0/24"`)
}
b.WriteString(`]}}`)
_, err := ExtractCIDRs(b.String(), "json", "items.x[]")
if err == nil {
t.Fatal("expected breadth error")
}
}
+42 -13
View File
@@ -5,11 +5,12 @@ import (
"io"
"net/http"
"strings"
"time"
"evobgp/internal/store"
)
// PrefetchCDNSourceETags performs conditional GETs for CDN module sources and updates stored ETags when the origin responds 200.
// PrefetchCDNSourceETags performs conditional GETs for CDN sources; on 200 parses CIDRs into module_prefix_snapshot.
func PrefetchCDNSourceETags(ctx context.Context, st store.Backend, hc *http.Client) error {
if hc == nil {
hc = http.DefaultClient
@@ -18,42 +19,70 @@ func PrefetchCDNSourceETags(ctx context.Context, st store.Backend, hc *http.Clie
if err != nil {
return err
}
now := time.Now().UTC()
for _, tid := range tenants {
for _, mod := range st.ListModules(tid) {
if !mod.Enabled || mod.Type != "CDN_CIDRS" {
if mod == nil || !mod.Enabled || mod.Type != "CDN_CIDRS" {
continue
}
omod, err := st.GetModule(tid, mod.ID)
if err != nil {
continue
}
sources, err := st.ListCDNSources(tid, mod.ID)
if err != nil {
continue
}
var prior []store.PrefixRow
if snap, ok, _ := st.GetModulePrefixSnapshot(tid, mod.ID); ok && snap != nil {
prior = snap.Prefixes
}
for _, src := range sources {
u := strings.TrimSpace(src.URL)
if u == "" {
if src == nil || strings.TrimSpace(src.URL) == "" {
continue
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimSpace(src.URL), nil)
if err != nil {
continue
}
if strings.TrimSpace(src.Etag) != "" {
req.Header.Set("If-None-Match", strings.TrimSpace(src.Etag))
if etag := strings.TrimSpace(src.Etag); etag != "" {
req.Header.Set("If-None-Match", etag)
}
resp, err := hc.Do(req)
if err != nil {
continue
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
_ = resp.Body.Close()
continue
}
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
continue
}
etag := strings.TrimSpace(resp.Header.Get("ETag"))
if etag == "" || etag == strings.TrimSpace(src.Etag) {
body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
_ = resp.Body.Close()
if err != nil {
continue
}
e := etag
_, _ = st.UpdateCDNSource(tid, mod.ID, src.ID, &store.CDNSourcePatch{Etag: &e})
prefixStrs, err := parseCDNBody(string(body), src)
if err != nil {
continue
}
newEtag := strings.TrimSpace(resp.Header.Get("ETag"))
patch := &store.CDNSourcePatch{LastRefreshedAt: &now}
if newEtag != "" && newEtag != strings.TrimSpace(src.Etag) {
e := newEtag
patch.Etag = &e
}
_, _ = st.UpdateCDNSource(tid, mod.ID, src.ID, patch)
rows := cdnRowsFromParsed(omod, src, prefixStrs)
_ = mergeCDNSourceIntoModuleSnapshot(st, tid, omod, src.ID, rows)
_ = prior // prior may be stale after merge; refresh for next source in loop
if snap, ok, _ := st.GetModulePrefixSnapshot(tid, mod.ID); ok && snap != nil {
prior = snap.Prefixes
}
}
}
}
+1028 -115
View File
File diff suppressed because it is too large. Load diff
+271
View File
@@ -0,0 +1,271 @@
package pipeline
import (
"context"
"net/http"
"testing"
"evobgp/internal/store"
)
func prefixRowSet(rows []store.PrefixRow) map[string]struct{} {
got := make(map[string]struct{}, len(rows))
for _, r := range rows {
c := prefixRowCommunity(r)
got[r.Prefix+"|"+c+"|"+r.Source] = struct{}{}
}
return got
}
func TestRefreshModule_AggregatesAllEnabledModules(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
mod2, err := m.CreateModule(tenant, &store.Module{Type: "IP_RANGES", Name: "extra-ip", Enabled: true, Priority: 30})
if err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.0.0.0/24"}); err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "192.168.0.0/24"}); err != nil {
t.Fatal(err)
}
ctx := context.Background()
before, _, _ := m.ListRevisions(tenant, "", "", 200)
beforeN := len(before)
rev1, err := RefreshModule(ctx, m, http.DefaultClient, tenant, modIP)
if err != nil {
t.Fatal(err)
}
after1, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after1) != beforeN+1 {
t.Fatalf("first refresh: want one new tenant revision, got %d -> %d", beforeN, len(after1))
}
px, _, _ := m.ListRevisionPrefixes(tenant, rev1, "", 1000)
if len(px) != 2 {
t.Fatalf("first refresh: want 2 aggregated prefixes, got %d: %+v", len(px), px)
}
rev2, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev2 == "" {
t.Fatal("second refresh: expected non-empty revision id")
}
after2, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after2) < len(after1) {
t.Fatalf("second refresh: revisions count should not decrease, had %d now %d", len(after1), len(after2))
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "10.0.1.0/24"}); err != nil {
t.Fatal(err)
}
rev3, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev3 == rev1 {
t.Fatal("after prefix change, expected a new revision")
}
after3, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after3) < len(after2) {
t.Fatalf("third refresh: revisions count should not decrease, had %d now %d", len(after2), len(after3))
}
px3, _, _ := m.ListRevisionPrefixes(tenant, rev3, "", 1000)
if len(px3) != 2 {
t.Fatalf("third refresh: smart aggregation should merge adjacent /24, want 2 prefixes, got %d", len(px3))
}
}
func TestSmartAggregatePrefixRows_RespectsCommunityAndSource(t *testing.T) {
commA := "c-a"
commB := "c-b"
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.1.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.2.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.3.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.4.0/24", CommunityID: &commB, Source: "ip_range"},
{Prefix: "10.0.5.0/24", CommunityID: &commB, Source: "cdn:x"},
}
out := smartAggregatePrefixRows(rows)
got := make(map[string]struct{}, len(out))
for _, r := range out {
c := ""
if r.CommunityID != nil {
c = *r.CommunityID
}
got[r.Prefix+"|"+c+"|"+r.Source] = struct{}{}
}
// First four /24 collapse into /22 because attributes are identical.
if _, ok := got["10.0.0.0/22|c-a|ip_range"]; !ok {
t.Fatalf("expected merged prefix for c-a/ip_range, got: %+v", out)
}
// Different community/source must stay separate.
if _, ok := got["10.0.4.0/24|c-b|ip_range"]; !ok {
t.Fatalf("expected distinct prefix for c-b/ip_range, got: %+v", out)
}
if _, ok := got["10.0.5.0/24|c-b|cdn:x"]; !ok {
t.Fatalf("expected distinct prefix for c-b/cdn:x, got: %+v", out)
}
if len(out) != 3 {
t.Fatalf("expected 3 resulting rows, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_PruneCoveredPrefixes(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/16", Source: "ip_range"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if len(out) != 1 {
t.Fatalf("expected 1 row after covered prune, got %d: %+v", len(out), out)
}
if _, ok := got["10.0.0.0/16||ip_range"]; !ok {
t.Fatalf("expected /16 only, got: %+v", out)
}
}
func TestSmartAggregatePrefixRows_MergeChainFourSlash24(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
{Prefix: "10.0.2.0/24", Source: "ip_range"},
{Prefix: "10.0.3.0/24", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if _, ok := got["10.0.0.0/22||ip_range"]; !ok {
t.Fatalf("expected merged /22, got: %+v", out)
}
if len(out) != 1 {
t.Fatalf("expected 1 row, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_DoesNotMergeAdjacentSlash8(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/8", Source: "ip_range"},
{Prefix: "11.0.0.0/8", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
if len(out) != 2 {
t.Fatalf("expected two /8 prefixes, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_MergesIPv6Slash64(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "2001:db8:0:0::/64", Source: "ip_range"},
{Prefix: "2001:db8:0:1::/64", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if _, ok := got["2001:db8::/63||ip_range"]; !ok {
t.Fatalf("expected merged IPv6 /63, got: %+v", out)
}
if len(out) != 1 {
t.Fatalf("expected 1 row, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_StableOutputOrder(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "192.168.0.0/24", Source: "cdn:b"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
}
out1 := smartAggregatePrefixRows(rows)
out2 := smartAggregatePrefixRows(rows)
if len(out1) != len(out2) {
t.Fatalf("length mismatch: %d vs %d", len(out1), len(out2))
}
for i := range out1 {
if out1[i].Prefix != out2[i].Prefix || out1[i].Source != out2[i].Source {
t.Fatalf("order not stable at %d: %+v vs %+v", i, out1[i], out2[i])
}
}
}
func TestRenderTenantRevisionFromPrefixes_SkipsWhenRedundantRawRows(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
ctx := context.Background()
baseRows := []store.PrefixRow{{Prefix: "10.0.0.0/16", Source: "ip_range"}}
rev1, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, baseRows)
if err != nil {
t.Fatal(err)
}
rev2, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, append(baseRows,
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
))
if err != nil {
t.Fatal(err)
}
if rev2 != rev1 {
t.Fatalf("expected hash skip for redundant covered prefix, got rev1=%s rev2=%s", rev1, rev2)
}
before, _, _ := m.ListRevisions(tenant, "", "", 200)
rev3, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, append(baseRows,
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
))
if err != nil {
t.Fatal(err)
}
if rev3 != rev1 {
t.Fatalf("expected hash skip for duplicate raw rows, got rev1=%s rev3=%s", rev1, rev3)
}
after, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after) != len(before) {
t.Fatalf("duplicate raw rows should not create revision, before=%d after=%d", len(before), len(after))
}
}
func TestHashAggregatedMaterialization_DedupesIdenticalRows(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
}
h1 := hashAggregatedMaterialization("tenant-a", rows)
h2 := hashAggregatedMaterialization("tenant-a", []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
})
if h1 != h2 {
t.Fatalf("expected deduped hash to match, got %s vs %s", h1, h2)
}
}
+24
View File
@@ -0,0 +1,24 @@
package pipeline
import (
"time"
"evobgp/internal/store"
)
// SchedulerTickSec matches the scheduler ticker interval (internal/scheduler).
const SchedulerTickSec = 30
// ModuleDueForScheduler reports whether a module's refresh interval bucket rolled since the last scheduler tick.
func ModuleDueForScheduler(mod *store.Module, now time.Time) bool {
if mod == nil || !mod.Enabled || mod.Type == "IP_RANGES" || mod.RefreshIntervalSec <= 0 {
return false
}
win := int64(mod.RefreshIntervalSec)
if win < 60 {
win = 60
}
cur := now.Unix() / win
prev := (now.Unix() - SchedulerTickSec) / win
return cur != prev
}
+80
View File
@@ -0,0 +1,80 @@
package pipeline
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"evobgp/internal/store"
)
// RefreshTenantModules ingests all listed modules in parallel and updates per-module snapshots.
func RefreshTenantModules(ctx context.Context, st store.Backend, hc *http.Client, tenantID string, moduleIDs []string) error {
if hc == nil {
hc = http.DefaultClient
}
var ids []string
seen := make(map[string]struct{})
for _, id := range moduleIDs {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
if len(ids) == 0 {
return nil
}
if len(ids) == 1 {
return RefreshModuleIngest(ctx, st, hc, tenantID, ids[0])
}
sem := make(chan struct{}, collectConcurrency())
errs := make([]error, len(ids))
var wg sync.WaitGroup
for i, mid := range ids {
wg.Add(1)
go func(idx int, moduleID string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
errs[idx] = RefreshModuleIngest(ctx, st, hc, tenantID, moduleID)
}(i, mid)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}
// PickTenantRefreshTriggerModule returns a module id for revision metadata (first enabled due module).
func PickTenantRefreshTriggerModule(st store.Backend, tenantID string, moduleIDs []string) (string, error) {
for _, id := range moduleIDs {
id = strings.TrimSpace(id)
if id == "" {
continue
}
mod, err := st.GetModule(tenantID, id)
if err != nil {
return "", err
}
if mod.Enabled {
return mod.ID, nil
}
}
for _, mod := range st.ListModules(tenantID) {
if mod != nil && mod.Enabled {
return mod.ID, nil
}
}
return "", fmt.Errorf("pipeline: no enabled module for tenant refresh")
}
Loaded 100 of 425 files, more files were not shown because too many files have changed in this diff. Show more