feat(api): add endpoint to list community prefixes with pagination
Introduced a new GET endpoint `/v1/communities/{id}/prefixes` to retrieve unique prefixes associated with a community, including pagination support via cursor and limit parameters. Updated OpenAPI documentation to reflect this addition. Implemented backend logic in both PostgreSQL and in-memory storage to handle the new functionality, ensuring proper authorization checks and response formatting.
This commit is contained in:
@@ -2922,6 +2922,57 @@ paths:
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/communities/{id}/prefixes:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
- $ref: "#/components/parameters/CommunityId"
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 500
|
||||
maximum: 5000
|
||||
get:
|
||||
tags: [Communities]
|
||||
summary: Префиксы community (latest revision per module)
|
||||
description: |
|
||||
Уникальные materialized-префиксы с данным community_id
|
||||
из последней ревизии каждого модуля tenant.
|
||||
Поле `prefixes` — плоский список для клиентов вроде EvoFirewall.
|
||||
operationId: listCommunityPrefixes
|
||||
responses:
|
||||
"200":
|
||||
description: Успешно.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [items, has_more]
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [prefix]
|
||||
properties:
|
||||
prefix:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
prefixes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
next_cursor:
|
||||
type: ["string", "null"]
|
||||
has_more:
|
||||
type: boolean
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
default:
|
||||
$ref: "#/components/responses/DefaultProblem"
|
||||
|
||||
/v1/communities/{id}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
|
||||
@@ -58,6 +58,7 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
||||
m.HandleFunc("GET /communities", s.handleListComm)
|
||||
m.HandleFunc("POST /communities", s.handlePostComm)
|
||||
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
||||
m.HandleFunc("GET /communities/{id}/prefixes", s.handleListCommPrefixes)
|
||||
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
||||
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
||||
|
||||
@@ -945,6 +946,39 @@ func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, commJSON(x))
|
||||
}
|
||||
|
||||
func (s *Server) handleListCommPrefixes(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requirePerm(w, a, "bgp:directories:read") {
|
||||
return
|
||||
}
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit == 0 {
|
||||
limit = 500
|
||||
}
|
||||
cursor := r.URL.Query().Get("cursor")
|
||||
rows, next, more, err := s.store.ListCommunityPrefixes(a.TenantID, r.PathValue("id"), cursor, limit)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
items := make([]map[string]any, 0, len(rows))
|
||||
prefixes := make([]string, 0, len(rows))
|
||||
for _, pr := range rows {
|
||||
m := map[string]any{"prefix": pr.Prefix}
|
||||
if pr.Source != "" {
|
||||
m["source"] = pr.Source
|
||||
}
|
||||
items = append(items, m)
|
||||
prefixes = append(prefixes, pr.Prefix)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": items,
|
||||
"prefixes": prefixes,
|
||||
"next_cursor": strPtrOrNull(next),
|
||||
"has_more": more,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handlePostComm(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requirePerm(w, a, "bgp:directories:write") {
|
||||
|
||||
@@ -1252,6 +1252,91 @@ func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) {
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]store.PrefixRow, string, bool, error) {
|
||||
if _, err := p.GetCommunity(tenantID, communityID); err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
if limit > 5000 {
|
||||
limit = 5000
|
||||
}
|
||||
off := 0
|
||||
if cursor != "" {
|
||||
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||
off = n
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
useSnap := prefixSnapshotTableExists(ctx, p.pool)
|
||||
var rows pgx.Rows
|
||||
var err error
|
||||
if useSnap {
|
||||
rows, err = p.pool.Query(ctx, `
|
||||
WITH latest AS (
|
||||
SELECT DISTINCT ON (module_id) id, prefix_snapshot_id
|
||||
FROM config_revision
|
||||
WHERE tenant_id = $1::uuid AND module_id IS NOT NULL
|
||||
ORDER BY module_id, created_at DESC
|
||||
),
|
||||
combined AS (
|
||||
SELECT rmp.prefix::text AS prefix, COALESCE(rmp.source, '') AS source
|
||||
FROM revision_materialized_prefix rmp
|
||||
JOIN latest l ON l.id = rmp.revision_id
|
||||
WHERE l.prefix_snapshot_id IS NULL AND rmp.community_id = $2::uuid
|
||||
UNION
|
||||
SELECT psr.prefix::text, COALESCE(psr.source, '')
|
||||
FROM prefix_snapshot_row psr
|
||||
JOIN latest l ON l.prefix_snapshot_id = psr.snapshot_id
|
||||
WHERE l.prefix_snapshot_id IS NOT NULL AND psr.community_id = $2::uuid
|
||||
)
|
||||
SELECT prefix, source FROM combined
|
||||
ORDER BY prefix
|
||||
LIMIT $3 OFFSET $4`, tenantID, communityID, limit+1, off)
|
||||
} else {
|
||||
rows, err = p.pool.Query(ctx, `
|
||||
WITH latest AS (
|
||||
SELECT DISTINCT ON (module_id) id
|
||||
FROM config_revision
|
||||
WHERE tenant_id = $1::uuid AND module_id IS NOT NULL
|
||||
ORDER BY module_id, created_at DESC
|
||||
)
|
||||
SELECT DISTINCT rmp.prefix::text, COALESCE(rmp.source, '')
|
||||
FROM revision_materialized_prefix rmp
|
||||
JOIN latest l ON l.id = rmp.revision_id
|
||||
WHERE rmp.community_id = $2::uuid
|
||||
ORDER BY 1
|
||||
LIMIT $3 OFFSET $4`, tenantID, communityID, limit+1, off)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var all []store.PrefixRow
|
||||
comm := communityID
|
||||
for rows.Next() {
|
||||
var pr store.PrefixRow
|
||||
if err := rows.Scan(&pr.Prefix, &pr.Source); err != nil {
|
||||
continue
|
||||
}
|
||||
pr.CommunityID = &comm
|
||||
all = append(all, pr)
|
||||
}
|
||||
more := len(all) > limit
|
||||
if more {
|
||||
all = all[:limit]
|
||||
}
|
||||
next := ""
|
||||
if more {
|
||||
next = fmt.Sprintf("%d", off+limit)
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
return all, next, more, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) CreateCommunity(tenantID string, in *store.Community) (*store.Community, error) {
|
||||
if in == nil {
|
||||
return nil, store.ErrInvalidInput
|
||||
|
||||
@@ -60,6 +60,8 @@ type Backend interface {
|
||||
CreateCommunity(tenantID string, in *Community) (*Community, error)
|
||||
UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error)
|
||||
DeleteCommunity(tenantID, id string) error
|
||||
// ListCommunityPrefixes returns unique prefixes tagged with community from latest revision per module.
|
||||
ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) (prefixes []PrefixRow, nextCursor string, hasMore bool, err error)
|
||||
|
||||
// ListPeers returns all BGP peers for a tenant (control plane may paginate in httpapi).
|
||||
ListPeers(tenantID string) []*BGPPeer
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -593,6 +595,74 @@ func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]PrefixRow, string, bool, error) {
|
||||
if _, err := m.GetCommunity(tenantID, communityID); err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
if limit > 5000 {
|
||||
limit = 5000
|
||||
}
|
||||
off := 0
|
||||
if cursor != "" {
|
||||
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||
off = n
|
||||
}
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
latestByModule := map[string]*Revision{}
|
||||
for _, rev := range m.revisions {
|
||||
if rev.TenantID != tenantID || strings.TrimSpace(rev.ModuleID) == "" {
|
||||
continue
|
||||
}
|
||||
cur := latestByModule[rev.ModuleID]
|
||||
if cur == nil || rev.CreatedAt.After(cur.CreatedAt) {
|
||||
latestByModule[rev.ModuleID] = rev
|
||||
}
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
var all []PrefixRow
|
||||
comm := communityID
|
||||
for _, rev := range latestByModule {
|
||||
for _, pr := range m.revPrefixes[rev.ID] {
|
||||
if pr.CommunityID == nil || *pr.CommunityID != communityID {
|
||||
continue
|
||||
}
|
||||
pfx := strings.TrimSpace(pr.Prefix)
|
||||
if pfx == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[pfx]; ok {
|
||||
continue
|
||||
}
|
||||
seen[pfx] = struct{}{}
|
||||
all = append(all, PrefixRow{Prefix: pfx, CommunityID: &comm, Source: pr.Source})
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].Prefix < all[j].Prefix })
|
||||
if off > len(all) {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
end := off + limit
|
||||
more := false
|
||||
next := ""
|
||||
if end < len(all) {
|
||||
more = true
|
||||
next = strconv.Itoa(end)
|
||||
all = all[off:end]
|
||||
} else {
|
||||
all = all[off:]
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
return all, next, more, nil
|
||||
}
|
||||
|
||||
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
|
||||
if in == nil || strings.TrimSpace(in.Community) == "" {
|
||||
return nil, ErrInvalidInput
|
||||
|
||||
Reference in New Issue
Block a user