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.
This commit is contained in:
+43
-10
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -231,17 +231,7 @@ 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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"evobgp/internal/birdfmt"
|
||||
@@ -16,6 +17,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MaterializedASPrefixKey returns the revision snapshot key for an AS-only entry (not a CIDR).
|
||||
func MaterializedASPrefixKey(asn int64) string {
|
||||
return fmt.Sprintf("as:%d", asn)
|
||||
}
|
||||
|
||||
// RefreshModule runs ingest (where applicable) and creates a new rendered revision for the module.
|
||||
func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenantID, moduleID string) (revisionID string, err error) {
|
||||
if hc == nil {
|
||||
@@ -50,7 +56,7 @@ func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenan
|
||||
return "", err
|
||||
}
|
||||
for _, e := range list {
|
||||
if e.Prefix == nil || strings.TrimSpace(*e.Prefix) == "" {
|
||||
if !store.ValidASN(e.ASN) {
|
||||
continue
|
||||
}
|
||||
comm := e.CommunityID
|
||||
@@ -58,7 +64,7 @@ func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenan
|
||||
c := *mod.DefaultCommunityID
|
||||
comm = &c
|
||||
}
|
||||
rows = append(rows, store.PrefixRow{Prefix: strings.TrimSpace(*e.Prefix), CommunityID: comm, Source: "as_entry"})
|
||||
rows = append(rows, store.PrefixRow{Prefix: MaterializedASPrefixKey(e.ASN), CommunityID: comm, Source: "as_entry"})
|
||||
}
|
||||
case "CDN_CIDRS":
|
||||
sources, err := st.ListCDNSources(tenantID, moduleID)
|
||||
@@ -175,8 +181,18 @@ func hashMaterialization(moduleID string, rows []store.PrefixRow) string {
|
||||
|
||||
func buildPreviewFragments(revisionID string, rows []store.PrefixRow) (map[string]string, error) {
|
||||
var v4, v6 []netip.Prefix
|
||||
var pathASNs []int64
|
||||
for _, pr := range rows {
|
||||
pfx, err := netip.ParsePrefix(strings.TrimSpace(pr.Prefix))
|
||||
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
|
||||
}
|
||||
pathASNs = append(pathASNs, n)
|
||||
continue
|
||||
}
|
||||
pfx, err := netip.ParsePrefix(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -186,11 +202,11 @@ func buildPreviewFragments(revisionID string, rows []store.PrefixRow) (map[strin
|
||||
v6 = append(v6, pfx.Masked())
|
||||
}
|
||||
}
|
||||
f4, err := birdfmt.RenderExportFilterIPv4("evobgp_export_v4", v4)
|
||||
f4, err := birdfmt.RenderExportFilterIPv4("evobgp_export_v4", v4, pathASNs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f6, err := birdfmt.RenderExportFilterIPv6("evobgp_export_v6", v6)
|
||||
f6, err := birdfmt.RenderExportFilterIPv6("evobgp_export_v6", v6, pathASNs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -830,7 +830,7 @@ func (p *Postgres) CreateRenderRevision(revisionID, tenantID, moduleID string, p
|
||||
}
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO revision_materialized_prefix (revision_id, prefix, community_id, source)
|
||||
VALUES ($1::uuid, $2::cidr, $3::uuid, $4)`,
|
||||
VALUES ($1::uuid, $2, $3::uuid, $4)`,
|
||||
strings.TrimSpace(revisionID), strings.TrimSpace(pr.Prefix), comm, src)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -164,7 +164,7 @@ func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, e
|
||||
}
|
||||
ctx := context.Background()
|
||||
rows, err := p.pool.Query(ctx, `
|
||||
SELECT id::text, asn, prefix::text, community_id::text FROM module_as_entry WHERE module_id=$1`, moduleID)
|
||||
SELECT id::text, asn, community_id::text FROM module_as_entry WHERE module_id=$1`, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -173,13 +173,10 @@ func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, e
|
||||
for rows.Next() {
|
||||
var e store.ASEntry
|
||||
e.ModuleID = moduleID
|
||||
var asn *int64
|
||||
var pref, comm *string
|
||||
if err := rows.Scan(&e.ID, &asn, &pref, &comm); err != nil {
|
||||
var comm *string
|
||||
if err := rows.Scan(&e.ID, &e.ASN, &comm); err != nil {
|
||||
continue
|
||||
}
|
||||
e.ASN = asn
|
||||
e.Prefix = pref
|
||||
e.CommunityID = strOrNil(comm)
|
||||
out = append(out, &e)
|
||||
}
|
||||
@@ -194,19 +191,15 @@ func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (
|
||||
if mod.Type != "AS_PREFIXES" {
|
||||
return nil, store.ErrInvalidInput
|
||||
}
|
||||
if in == nil || (in.ASN == nil && (in.Prefix == nil || strings.TrimSpace(*in.Prefix) == "")) {
|
||||
if in == nil || !store.ValidASN(in.ASN) {
|
||||
return nil, store.ErrInvalidInput
|
||||
}
|
||||
ctx := context.Background()
|
||||
id := uuid.NewString()
|
||||
var pref any
|
||||
if in.Prefix != nil && strings.TrimSpace(*in.Prefix) != "" {
|
||||
pref = strings.TrimSpace(*in.Prefix)
|
||||
}
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
INSERT INTO module_as_entry (id, module_id, asn, prefix, community_id)
|
||||
VALUES ($1,$2,$3,$4::cidr, NULLIF($5::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||
id, moduleID, in.ASN, pref, uuidOrNilPtr(in.CommunityID))
|
||||
INSERT INTO module_as_entry (id, module_id, asn, community_id)
|
||||
VALUES ($1,$2,$3,NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||
id, moduleID, in.ASN, uuidOrNilPtr(in.CommunityID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,16 +209,13 @@ func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (
|
||||
func (p *Postgres) getASEntry(ctx context.Context, moduleID, id string) (*store.ASEntry, error) {
|
||||
var e store.ASEntry
|
||||
e.ModuleID = moduleID
|
||||
var asn *int64
|
||||
var pref, comm *string
|
||||
var comm *string
|
||||
err := p.pool.QueryRow(ctx, `
|
||||
SELECT id::text, asn, prefix::text, community_id::text FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
|
||||
&e.ID, &asn, &pref, &comm)
|
||||
SELECT id::text, asn, community_id::text FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
|
||||
&e.ID, &e.ASN, &comm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.ASN = asn
|
||||
e.Prefix = pref
|
||||
e.CommunityID = strOrNil(comm)
|
||||
return &e, nil
|
||||
}
|
||||
@@ -242,15 +232,7 @@ func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *stor
|
||||
return nil, err
|
||||
}
|
||||
if patch.ASN != nil {
|
||||
cur.ASN = patch.ASN
|
||||
}
|
||||
if patch.Prefix != nil {
|
||||
p := strings.TrimSpace(*patch.Prefix)
|
||||
if p == "" {
|
||||
cur.Prefix = nil
|
||||
} else {
|
||||
cur.Prefix = &p
|
||||
}
|
||||
cur.ASN = *patch.ASN
|
||||
}
|
||||
if patch.CommunityID != nil {
|
||||
v := strings.TrimSpace(*patch.CommunityID)
|
||||
@@ -260,15 +242,14 @@ func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *stor
|
||||
cur.CommunityID = &v
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
var pref any
|
||||
if cur.Prefix != nil {
|
||||
pref = *cur.Prefix
|
||||
if !store.ValidASN(cur.ASN) {
|
||||
return nil, store.ErrInvalidInput
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
UPDATE module_as_entry SET asn=$3, prefix=$4::cidr, community_id=NULLIF($5::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||
WHERE id=$1 AND module_id=$2`,
|
||||
entryID, moduleID, cur.ASN, pref, uuidOrNilPtr(cur.CommunityID))
|
||||
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -109,19 +109,22 @@ type CDNSourcePatch struct {
|
||||
}
|
||||
|
||||
type ASEntry struct {
|
||||
ID string
|
||||
ModuleID string
|
||||
ASN *int64
|
||||
Prefix *string
|
||||
CommunityID *string
|
||||
ID string
|
||||
ModuleID string
|
||||
ASN int64
|
||||
CommunityID *string
|
||||
}
|
||||
|
||||
type ASEntryPatch struct {
|
||||
ASN *int64
|
||||
Prefix *string
|
||||
CommunityID *string
|
||||
}
|
||||
|
||||
// ValidASN reports whether n is a usable BGP ASN (1..4294967295).
|
||||
func ValidASN(n int64) bool {
|
||||
return n >= 1 && n <= 4294967295
|
||||
}
|
||||
|
||||
type DomainEntry struct {
|
||||
ID string
|
||||
ModuleID string
|
||||
|
||||
@@ -217,7 +217,7 @@ func (m *Memory) ListASEntries(tenantID, moduleID string) ([]*ASEntry, error) {
|
||||
}
|
||||
|
||||
func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry, error) {
|
||||
if in == nil || (in.ASN == nil && (in.Prefix == nil || strings.TrimSpace(*in.Prefix) == "")) {
|
||||
if in == nil || !ValidASN(in.ASN) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
@@ -230,7 +230,7 @@ func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
id := uuid.NewString()
|
||||
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, Prefix: in.Prefix, CommunityID: in.CommunityID}
|
||||
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, CommunityID: in.CommunityID}
|
||||
m.asEntries[id] = e
|
||||
return e, nil
|
||||
}
|
||||
@@ -249,15 +249,7 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if patch.ASN != nil {
|
||||
e.ASN = patch.ASN
|
||||
}
|
||||
if patch.Prefix != nil {
|
||||
p := strings.TrimSpace(*patch.Prefix)
|
||||
if p == "" {
|
||||
e.Prefix = nil
|
||||
} else {
|
||||
e.Prefix = &p
|
||||
}
|
||||
e.ASN = *patch.ASN
|
||||
}
|
||||
if patch.CommunityID != nil {
|
||||
v := strings.TrimSpace(*patch.CommunityID)
|
||||
@@ -267,6 +259,9 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
|
||||
e.CommunityID = &v
|
||||
}
|
||||
}
|
||||
if !ValidASN(e.ASN) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user