docs: enhance EvoBGP architecture plan with new risk mitigation strategies, testing matrix for BIRD2, and CI/CD workflow details. Updated section titles for clarity and added completed todos for deployment practices.
CI / openapi (push) Successful in 1m35s
CI / go (push) Failing after 16s
CI / bird2 (push) Has been skipped

This commit is contained in:
Denozordec
2026-04-04 01:27:17 +07:00
parent 4480d64a7f
commit 3992d01c3e
30 changed files with 621 additions and 2 deletions
+64
View File
@@ -0,0 +1,64 @@
package birdfmt
import (
"embed"
"io/fs"
"path"
"sort"
)
//go:embed testdata/scenarios
var scenariosFS embed.FS
// BirdScenarioPaths returns POSIX-style directory paths (under testdata/scenarios)
// that contain a bird.conf file, sorted for stable iteration.
func BirdScenarioPaths() ([]string, error) {
var dirs []string
err := fs.WalkDir(scenariosFS, "testdata/scenarios", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || d.Name() != "bird.conf" {
return nil
}
dir := path.Dir(p)
if dir == "testdata/scenarios" {
return nil
}
dirs = append(dirs, dir)
return nil
})
if err != nil {
return nil, err
}
sort.Strings(dirs)
return dirs, nil
}
// ScenarioBirdConfRel returns the path relative to module root internal/birdfmt/ for bird.conf.
func ScenarioBirdConfRel(scenarioDir string) string {
return path.Join(scenarioDir, "bird.conf")
}
// ReadScenarioBirdConf returns the contents of bird.conf for a scenario directory from embed.
func ReadScenarioBirdConf(scenarioDir string) ([]byte, error) {
return scenariosFS.ReadFile(path.Join(scenarioDir, "bird.conf"))
}
// RepoRelativeBirdConf is the path from the repository root (for CI / docker bind mounts).
func RepoRelativeBirdConf(scenarioDir string) string {
return path.Join("internal/birdfmt", scenarioDir, "bird.conf")
}
// AllScenarioNames returns leaf scenario names (last path segment) for tables / docs.
func AllScenarioNames() ([]string, error) {
paths, err := BirdScenarioPaths()
if err != nil {
return nil, err
}
names := make([]string, len(paths))
for i, p := range paths {
names[i] = path.Base(p)
}
return names, nil
}
+68
View File
@@ -0,0 +1,68 @@
package birdfmt
import (
"path"
"slices"
"strings"
"testing"
)
func TestBirdScenarioPaths_Table(t *testing.T) {
paths, err := BirdScenarioPaths()
if err != nil {
t.Fatal(err)
}
// Expected scenario leaf names — extend when adding directories under testdata/scenarios.
wantNames := []string{
"bgp_dual_peer",
"bgp_ipv4_peer",
"bgp_ipv6_peer",
"domains_resolved",
"empty_static",
"filter_export",
"large_prefix_list",
"mixed_static_bgp",
"minimal",
"static_ipv4",
"with_include",
}
var gotNames []string
for _, p := range paths {
gotNames = append(gotNames, path.Base(p))
}
slices.Sort(gotNames)
if !slices.Equal(gotNames, wantNames) {
t.Fatalf("scenario set drift: got %v want %v", gotNames, wantNames)
}
for _, p := range paths {
t.Run(path.Base(p), func(t *testing.T) {
b, err := ReadScenarioBirdConf(p)
if err != nil {
t.Fatal(err)
}
if len(b) == 0 {
t.Fatal("empty bird.conf")
}
if !strings.Contains(string(b), "router id") {
t.Fatal("expected router id in scenario")
}
})
}
}
func TestRepoRelativeBirdConf_Table(t *testing.T) {
paths, err := BirdScenarioPaths()
if err != nil {
t.Fatal(err)
}
for _, p := range paths {
rel := RepoRelativeBirdConf(p)
if !strings.HasPrefix(rel, "internal/birdfmt/") {
t.Fatalf("%s: bad prefix %q", p, rel)
}
if !strings.HasSuffix(rel, "/bird.conf") {
t.Fatalf("%s: expected .../bird.conf, got %q", p, rel)
}
}
}
+39
View File
@@ -0,0 +1,39 @@
package birdfmt
import (
"net/netip"
"sort"
"strings"
)
// RenderStaticIPv4Protocol renders a BIRD 2 `protocol static` block for IPv4 prefixes.
// Prefixes are sorted for stable output (golden tests, reproducible rev).
func RenderStaticIPv4Protocol(protocolName string, prefixes []netip.Prefix) string {
if protocolName == "" {
protocolName = "evobgp_static_v4"
}
uniq := make(map[string]netip.Prefix)
for _, p := range prefixes {
if !p.Addr().Is4() {
continue
}
uniq[p.String()] = p.Masked()
}
keys := make([]string, 0, len(uniq))
for k := range uniq {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
b.WriteString("protocol static ")
b.WriteString(protocolName)
b.WriteString(" {\n ipv4;\n")
for _, k := range keys {
b.WriteString(" route ")
b.WriteString(k)
b.WriteString(" unreachable;\n")
}
b.WriteString("}\n")
return b.String()
}
+34
View File
@@ -0,0 +1,34 @@
package birdfmt
import (
_ "embed"
"net/netip"
"strings"
"testing"
)
//go:embed testdata/golden/static_ipv4_small.golden
var goldenStaticIPv4Small string
func TestRenderStaticIPv4Protocol_Golden(t *testing.T) {
p1 := netip.MustParsePrefix("203.0.113.0/24")
p2 := netip.MustParsePrefix("198.51.100.0/24")
// duplicate to assert dedup
got := RenderStaticIPv4Protocol("evobgp_test", []netip.Prefix{p1, p2, p1})
want := strings.TrimSuffix(goldenStaticIPv4Small, "\n")
got = strings.TrimSuffix(got, "\n")
if got != want {
t.Fatalf("golden mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}
func TestRenderStaticIPv4Protocol_Empty(t *testing.T) {
got := RenderStaticIPv4Protocol("evobgp_empty", nil)
if !strings.Contains(got, "protocol static evobgp_empty") {
t.Fatal(got)
}
if !strings.Contains(got, "ipv4") {
t.Fatal(got)
}
}
@@ -0,0 +1,5 @@
protocol static evobgp_test {
ipv4;
route 198.51.100.0/24 unreachable;
route 203.0.113.0/24 unreachable;
}
@@ -0,0 +1,28 @@
# tags: peer, ipv4, ipv6, mixed
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol bgp peer_a {
local 192.0.2.1 as 65001;
neighbor 192.0.2.2 as 65002;
ipv4 {
import all;
export all;
};
}
protocol bgp peer_b {
local fd00:ebgp::1 as 65001;
neighbor fd00:ebgp::3 as 65003;
ipv6 {
import all;
export all;
};
}
@@ -0,0 +1,19 @@
# tags: peer, ipv4, AS_PREFIXES
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol bgp peer_v4 {
local 192.0.2.1 as 65001;
neighbor 192.0.2.2 as 65002;
ipv4 {
import all;
export all;
};
}
@@ -0,0 +1,19 @@
# tags: peer, ipv6, AS_PREFIXES
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol bgp peer_v6 {
local fd00:ebgp::1 as 65001;
neighbor fd00:ebgp::2 as 65002;
ipv6 {
import all;
export all;
};
}
@@ -0,0 +1,18 @@
# tags: DOMAINS, module_output
# DOMAINS modules materialize to prefixes before render; same shape as static IPv4.
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol static evobgp_from_domains {
ipv4;
route 192.0.2.10/32 unreachable;
route 192.0.2.11/32 unreachable;
}
@@ -0,0 +1,16 @@
# tags: edge, IP_RANGES
# Empty static IPv4 table (generator must emit valid empty protocol).
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol static evobgp_empty {
ipv4;
}
@@ -0,0 +1,26 @@
# tags: community, filter, peer
# Export filter (placeholder for community-aware export).
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
filter evobgp_export_v4 {
if net ~ [ 203.0.113.0/24 ] then accept;
reject;
}
protocol bgp peer_filtered {
local 192.0.2.1 as 65001;
neighbor 192.0.2.2 as 65002;
ipv4 {
import all;
export filter evobgp_export_v4;
};
}
@@ -0,0 +1,12 @@
# tags: stress, CDN_CIDRS
router id 192.0.2.1;
include "bird.d/bulk_static.conf";
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
@@ -0,0 +1,23 @@
protocol static evobgp_bulk {
ipv4;
route 10.200.0.0/24 unreachable;
route 10.200.1.0/24 unreachable;
route 10.200.2.0/24 unreachable;
route 10.200.3.0/24 unreachable;
route 10.200.4.0/24 unreachable;
route 10.200.5.0/24 unreachable;
route 10.200.6.0/24 unreachable;
route 10.200.7.0/24 unreachable;
route 10.200.8.0/24 unreachable;
route 10.200.9.0/24 unreachable;
route 10.200.10.0/24 unreachable;
route 10.200.11.0/24 unreachable;
route 10.200.12.0/24 unreachable;
route 10.200.13.0/24 unreachable;
route 10.200.14.0/24 unreachable;
route 10.200.15.0/24 unreachable;
route 10.200.16.0/24 unreachable;
route 10.200.17.0/24 unreachable;
route 10.200.18.0/24 unreachable;
route 10.200.19.0/24 unreachable;
}
+12
View File
@@ -0,0 +1,12 @@
# tags: baseline, master
# Minimal BIRD 2 skeleton (device + direct), no EvoBGP-generated fragments yet.
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
@@ -0,0 +1,27 @@
# tags: IP_RANGES, CDN_CIDRS, peer, combined
# Combined static materialization + BGP session (multi-module revision sketch).
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol static evobgp_prefixes {
ipv4;
route 203.0.113.0/24 unreachable;
route 203.0.114.0/24 unreachable;
}
protocol bgp uplink {
local 192.0.2.1 as 65001;
neighbor 192.0.2.2 as 65002;
ipv4 {
import all;
export all;
};
}
@@ -0,0 +1,18 @@
# tags: IP_RANGES, module_output
# Static IPv4 routes only (materialized prefix list style).
router id 192.0.2.1;
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
protocol static evobgp_v4 {
ipv4;
route 198.51.100.0/24 unreachable;
route 203.0.113.0/24 unreachable;
}
@@ -0,0 +1,14 @@
# tags: baseline, include
# Operator shell includes generated fragments from bird.d/
router id 192.0.2.1;
include "bird.d/static.conf";
protocol device {
}
protocol direct {
ipv4;
ipv6;
}
@@ -0,0 +1,4 @@
protocol static evobgp_inc {
ipv4;
route 203.0.113.0/24 unreachable;
}