fix(api): сообщать об отказах общего GRE-каталога

This commit is contained in:
Denozordec committed 2026-09-30 15:31:09 +07:00
1 parent d112935c73
commit 6f00596e33
7 files changed
+65 -11

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@
"test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts",
"test:ros-file": "tsx src/services/ros-file-contents.test.ts",
"test:backups": "tsx src/services/s3-backup-client.test.ts",
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts",
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts",
"test": "npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:live-maps && npm run test:config-sync && npm run test:ros-file",
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts"
},
@@ -0,0 +1,15 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import filtersRoutes from "./filters.js"
const app = Fastify()
app.setValidatorCompiler(validatorCompiler)
app.setSerializerCompiler(serializerCompiler)
await app.register(filtersRoutes, { prefix: "/api" })
for (const id of ["srv1", "0", "1abc", "-1"]) {
const response = await app.inject({ method: "GET", url: "/api/filters/gre-tunnels?serverId=" + encodeURIComponent(id) })
assert.equal(response.statusCode, 400)
assert.equal(response.json().error, "Некорректный serverId GRE-каталога")
}
await app.close()
console.log("GRE invalid selected context: HTTP 400 without DB/RouterOS passed")
+3 -9
View File
@@ -4,6 +4,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { filterRules, recursiveRoutes, servers } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { pollGreCatalog } from "../services/gre-catalog-poll.js"
import { parseDbServerId } from "../utils/server-id.js"
import { appendEvent } from "../modules/events/service/events-service.js"
import { managedComment } from "../managed-markers.js"
@@ -464,6 +465,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/filters/gre-tunnels", async (req, reply) => {
const q = req.query as { serverId?: string | number }
const sid = parseDbServerId(q.serverId)
if (q.serverId !== undefined && sid === null) return reply.status(400).send({ error: "Некорректный serverId GRE-каталога" })
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
if (sid !== null) {
@@ -478,15 +480,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
}
}
const results = await Promise.all(enabledServers.map(async (server) => {
try {
const remote = await fetchServerFilters(server)
return remote.tunnels
} catch {
return [] as LiveGreTunnel[]
}
}))
return reply.send({ tunnels: results.flat() })
return reply.send(await pollGreCatalog(enabledServers, fetchServerFilters))
})
/** Без serverId — cache для дашборда. С serverId — live с CHR, cache fallback. */
@@ -0,0 +1,17 @@
import assert from "node:assert/strict"
import { pollGreCatalog } from "./gre-catalog-poll.js"
const devices = [{ id: 1, name: "Первый" }, { id: 2, name: "Второй" }]
const mixed = await pollGreCatalog(devices, async device => {
if (device.id === 2) throw new Error("Таймаут тест")
return { tunnels: [{ id: "*1", serverId: "1" }] }
})
assert.deepEqual(mixed, { tunnels: [{ id: "*1", serverId: "1" }], failures: [{ serverId: "2", serverName: "Второй", error: "Таймаут тест" }] })
assert.deepEqual(await pollGreCatalog(devices, async () => ({ tunnels: [] })), { tunnels: [], failures: [] })
const failed = await pollGreCatalog(devices, async () => { throw new Error("Недоступно") })
assert.equal(failed.tunnels.length, 0)
assert.deepEqual(failed.failures.map(f => f.serverId), ["1", "2"])
const foreign = await pollGreCatalog([devices[0]], async () => ({ tunnels: [{ serverId: "2" }] }))
assert.equal(foreign.tunnels.length, 0)
assert.equal(foreign.failures[0].serverId, "1")
assert.deepEqual(await pollGreCatalog([], async () => { throw new Error("Не должен вызываться") }), { tunnels: [], failures: [] })
console.log("gre catalog: partial/empty/failed/foreign contexts passed")
+14
View File
@@ -0,0 +1,14 @@
export async function pollGreCatalog<S extends { id: number; name: string }, T extends { serverId: string }>(
devices: readonly S[], read: (device: S) => Promise<{ tunnels: T[] }>,
): Promise<{ tunnels: T[]; failures: Array<{ serverId: string; serverName: string; error: string }> }> {
const results = await Promise.all(devices.map(async device => {
try {
const { tunnels } = await read(device)
if (!Array.isArray(tunnels) || tunnels.some(t => t.serverId !== String(device.id))) throw new Error("Чужой контекст GRE-каталога")
return { tunnels, failures: [] }
} catch (error) {
return { tunnels: [], failures: [{ serverId: String(device.id), serverName: device.name, error: error instanceof Error ? error.message : String(error) }] }
}
}))
return { tunnels: results.flatMap(r => r.tunnels), failures: results.flatMap(r => r.failures) }
}