feat: add aggregated router-lists catalog endpoint and update module listing filters
CI / changes (push) Successful in 5s
CI / openapi (push) Successful in 22s
CI / go (push) Successful in 38s
CI / bird2 (push) Has been cancelled
CI / docker-go-prime (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-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
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-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / openapi (push) Successful in 22s
CI / go (push) Successful in 38s
CI / bird2 (push) Has been cancelled
CI / docker-go-prime (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-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
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-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (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.
This commit is contained in:
+85
-1
@@ -933,7 +933,7 @@ paths:
|
||||
get:
|
||||
tags: [Modules]
|
||||
summary: Список модулей
|
||||
description: Модули tenant с опциональными фильтрами по типу и флагу `enabled`.
|
||||
description: Модули tenant с опциональными фильтрами по типу и флагу `enabled` (фильтры применяются сервером).
|
||||
operationId: listModules
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TenantId"
|
||||
@@ -962,6 +962,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: Создать модуль
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
| 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 модель | Комментарий |
|
||||
|
||||
@@ -47,6 +47,7 @@ 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)
|
||||
@@ -161,9 +162,27 @@ func (s *Server) handleListModules(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.requireAtLeast(w, a, "viewer") {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
mods := s.store.ListModules(a.TenantID)
|
||||
items := make([]map[string]any, 0, len(mods))
|
||||
for _, mod := range mods {
|
||||
if typeFilter != "" && mod.Type != typeFilter {
|
||||
continue
|
||||
}
|
||||
if enabledFilter != nil && mod.Enabled != *enabledFilter {
|
||||
continue
|
||||
}
|
||||
items = append(items, moduleJSON(mod))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
@@ -171,6 +190,103 @@ func (s *Server) handleListModules(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
mods := s.store.ListModules(a.TenantID)
|
||||
moduleItems := make([]map[string]any, 0, len(mods))
|
||||
domains := make([]map[string]any, 0)
|
||||
asns := make([]map[string]any, 0)
|
||||
ipRanges := make([]map[string]any, 0)
|
||||
|
||||
for _, mod := range mods {
|
||||
switch mod.Type {
|
||||
case "DOMAINS", "AS_PREFIXES", "IP_RANGES":
|
||||
moduleItems = append(moduleItems, moduleJSON(mod))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
switch mod.Type {
|
||||
case "DOMAINS":
|
||||
list, err := s.store.ListDomainEntries(a.TenantID, mod.ID)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
for _, x := range list {
|
||||
domains = append(domains, map[string]any{
|
||||
"module_id": mod.ID,
|
||||
"entry": domainEntryJSON(x),
|
||||
})
|
||||
}
|
||||
case "AS_PREFIXES":
|
||||
list, err := s.store.ListASEntries(a.TenantID, mod.ID)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
for _, x := range list {
|
||||
asns = append(asns, map[string]any{
|
||||
"module_id": mod.ID,
|
||||
"entry": asEntryJSON(x),
|
||||
})
|
||||
}
|
||||
case "IP_RANGES":
|
||||
list, err := s.store.ListIPRangeEntries(a.TenantID, mod.ID)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
for _, x := range list {
|
||||
ipRanges = append(ipRanges, map[string]any{
|
||||
"module_id": mod.ID,
|
||||
"entry": ipRangeJSON(x),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comms, err := s.store.ListCommunities(a.TenantID)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
communityItems := make([]map[string]any, 0, len(comms))
|
||||
for _, c := range comms {
|
||||
communityItems = append(communityItems, map[string]any{
|
||||
"id": c.ID,
|
||||
"community": c.Community,
|
||||
"title": c.Title,
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"modules": map[string]any{
|
||||
"items": moduleItems,
|
||||
},
|
||||
"domains": map[string]any{
|
||||
"items": domains,
|
||||
},
|
||||
"asns": map[string]any{
|
||||
"items": asns,
|
||||
},
|
||||
"ip_ranges": map[string]any{
|
||||
"items": ipRanges,
|
||||
},
|
||||
"communities": map[string]any{
|
||||
"items": communityItems,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetModule(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok {
|
||||
|
||||
@@ -149,6 +149,76 @@ 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 queues job", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/revisions/"+rev+"/rollback", nil)
|
||||
req.Header.Set("Authorization", "Bearer opkey")
|
||||
|
||||
@@ -21,6 +21,14 @@ export type ModuleRow = {
|
||||
};
|
||||
export type ModulesResponse = Page<ModuleRow>;
|
||||
|
||||
export type RouterListsCatalogResponse = {
|
||||
modules: { items: ModuleRow[] };
|
||||
domains: { items: { module_id: string; entry: DomainEntry }[] };
|
||||
asns: { items: { module_id: string; entry: AsEntry }[] };
|
||||
ip_ranges: { items: { module_id: string; entry: IpRangeEntry }[] };
|
||||
communities: { items: BgpCommunity[] };
|
||||
};
|
||||
|
||||
export type ModuleCreate = {
|
||||
type: ModuleType;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user