fix(containers): исключить опрос выключенных серверов и ложные пустые данные
This commit is contained in:
1 parent
74151176d7
commit
dc7b22940a
16 files changed
+470
-88
No files matched your search
@@ -36,7 +36,7 @@
|
||||
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||
"test:geoip": "tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts",
|
||||
"test:containers": "tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
|
||||
"test:containers": "tsx src/services/containers-live.test.ts && tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
|
||||
"test:certificates": "tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts",
|
||||
"test:system-database": "tsx src/services/system-database-partition-restore.test.ts && tsx src/services/system-database-cache.test.ts && tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts",
|
||||
"test:all": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && 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:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database && npm run test:server-privacy",
|
||||
|
||||
@@ -17,14 +17,21 @@ failed=true;assert.equal((await read()).statusCode,500)
|
||||
assert.equal(permissionForRequest("GET","/api/containers/state"),"mm:dashboard:read")
|
||||
assert.equal(permissionForRequest("POST","/api/servers/1/containers/start"),"mm:servers:write")
|
||||
await app.close();console.log("Containers HTTP: explicit empty/partial error, invalid state 502, DB failure 500, existing permission contract")
|
||||
let enabled = true, missing = false, calls = 0, removalError: Error | undefined
|
||||
let enabled = true, missing = false, calls = 0, reads = 0, failRead = false, removalError: Error | undefined
|
||||
const readError = new Error("RouterOS refused")
|
||||
const mutations = Fastify();mutations.setValidatorCompiler(validatorCompiler);mutations.setSerializerCompiler(serializerCompiler)
|
||||
await mutations.register(createContainersRoutes({getServer:async()=>missing?null:({id:1,enabled} as NonNullable<Awaited<ReturnType<NonNullable<Parameters<typeof createContainersRoutes>[0]>["getServer"]>>>),remove:async()=>{calls++;if(removalError)throw removalError}}),{prefix:"/api"})
|
||||
await mutations.register(createContainersRoutes({getServer:async()=>missing?null:({id:1,enabled} as NonNullable<Awaited<ReturnType<NonNullable<Parameters<typeof createContainersRoutes>[0]>["getServer"]>>>),getLegacyCatalog:async()=>{if(failRead)throw readError;return []},getServerContainers:async()=>{reads++;if(failRead)throw readError;return []},remove:async()=>{calls++;if(removalError)throw removalError}}),{prefix:"/api"})
|
||||
const remove = ()=>mutations.inject({method:"POST",url:"/api/servers/1/containers/remove",payload:{rosId:"*A"}})
|
||||
enabled=false;assert.equal((await remove()).statusCode,409);assert.equal(calls,0)
|
||||
missing=true;assert.equal((await remove()).statusCode,404);assert.equal(calls,0)
|
||||
missing=false;enabled=true;removalError=new ContainerRemovalConflict("Остановите контейнер");assert.equal((await remove()).statusCode,409)
|
||||
removalError=new Error("read failed");assert.equal((await remove()).statusCode,502)
|
||||
removalError=undefined;assert.deepEqual((await remove()).json(),{ok:true})
|
||||
const readSelected=()=>mutations.inject({method:"GET",url:"/api/servers/1/containers"})
|
||||
enabled=false;assert.equal((await readSelected()).statusCode,409);assert.equal(reads,0)
|
||||
missing=true;assert.equal((await readSelected()).statusCode,404);assert.equal(reads,0)
|
||||
missing=false;enabled=true;assert.equal((await readSelected()).statusCode,200);assert.equal(reads,1)
|
||||
failRead=true;assert.equal((await readSelected()).statusCode,502)
|
||||
assert.equal((await mutations.inject({method:"GET",url:"/api/containers"})).statusCode,502)
|
||||
assert.equal(permissionForRequest("POST","/api/servers/1/containers/remove"),"mm:servers:write")
|
||||
await mutations.close();console.log("Containers mutations HTTP: disabled/missing server cannot dispatch; removal conflict 409, uncertainty 502, confirmed success and write permission")
|
||||
@@ -21,7 +21,7 @@ const RosIdBodySchema = z.object({
|
||||
type RosIdBody = z.infer<typeof RosIdBodySchema>
|
||||
type MutateFn = typeof startContainer
|
||||
|
||||
export function createContainersRoutes(deps: { getState?: typeof listContainersState; getServer?: typeof getEnabledServerById; remove?: MutateFn } = {}): FastifyPluginAsyncZod {
|
||||
export function createContainersRoutes(deps: { getState?: typeof listContainersState; getServer?: typeof getEnabledServerById; getLegacyCatalog?: typeof listContainers; getServerContainers?: typeof listContainersForServer; remove?: MutateFn } = {}): FastifyPluginAsyncZod {
|
||||
return async (app) => {
|
||||
app.get("/containers/state", async (_req, reply) => {
|
||||
const state = containersStateSchema.safeParse(await (deps.getState ?? listContainersState)())
|
||||
@@ -29,16 +29,25 @@ export function createContainersRoutes(deps: { getState?: typeof listContainersS
|
||||
return reply.send(state.data)
|
||||
})
|
||||
app.get("/containers", async (_req, reply) => {
|
||||
const containers = await listContainers()
|
||||
return reply.send({ containers })
|
||||
try {
|
||||
const containers = await (deps.getLegacyCatalog ?? listContainers)()
|
||||
return reply.send({ containers })
|
||||
} catch (error) {
|
||||
return reply.status(502).send({ error: error instanceof Error ? error.message : "Не удалось получить контейнеры" })
|
||||
}
|
||||
})
|
||||
|
||||
app.get("/servers/:id/containers", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = await getEnabledServerById(params.id)
|
||||
const server = await (deps.getServer ?? getEnabledServerById)(params.id)
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
const containers = await listContainersForServer(server)
|
||||
return reply.send({ containers })
|
||||
if (!server.enabled) return reply.status(409).send({ error: "Сервер выключен. Опрос контейнеров недоступен." })
|
||||
try {
|
||||
const containers = await (deps.getServerContainers ?? listContainersForServer)(server)
|
||||
return reply.send({ containers })
|
||||
} catch (error) {
|
||||
return reply.status(502).send({ error: error instanceof Error ? error.message : "Не удалось получить контейнеры" })
|
||||
}
|
||||
})
|
||||
|
||||
function registerMutate(path: string, fn: MutateFn) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { mapContainerRow } from "./containers-live.js"
|
||||
import { mapContainerRow, listContainersForServer, getEnabledServerById } from "./containers-live.js"
|
||||
import { createServer } from "node:http"
|
||||
|
||||
const server = {
|
||||
id: 3,
|
||||
@@ -44,3 +45,31 @@ assert.deepEqual(row.mounts, [
|
||||
assert.equal(row.startOnBoot, true)
|
||||
|
||||
console.log("containers-live.test.ts: ok")
|
||||
for(const [native,expected] of [[{running:"true"},"running"],[{stopped:"true"},"stopped"],[{},"unknown"],[{running:true,stopped:true},"unknown"]] as const){
|
||||
const result=mapContainerRow(server,{".id":"*B",...native},[],[],0)
|
||||
assert.equal(result.status,expected);assert.equal(result.startOnBoot,null);assert.equal(result.image,"");assert.equal(result.tag,"")
|
||||
}
|
||||
const modern=mapContainerRow(server,{".id":"*C",envlists:"first,second",mountlists:"config","memory-current":"0","memory-high":"52428800",cpu:"0"},[{list:"first",key:"A",value:"1"},{list:"second",key:"B",value:"2"}],[{list:"config",dst:"/etc"}],0)
|
||||
assert.equal(modern.envs.length,2);assert.equal(modern.mounts.length,1);assert.equal(modern.memMb,0);assert.equal(modern.cpu,0)
|
||||
const limitOnly=mapContainerRow(server,{".id":"*D","memory-high":"52428800",cpu:"5bad"},[],[],0)
|
||||
assert.equal(limitOnly.memMb,undefined);assert.equal(limitOnly.cpu,undefined)
|
||||
for(const invalid of ["1abc","","0","-1","1.5","9007199254740993"])assert.equal(await getEnabledServerById(invalid),null)
|
||||
let mode="ok",requests=0
|
||||
const router=createServer((req,res)=>{
|
||||
requests++;res.setHeader("Content-Type","application/json")
|
||||
if(mode==="unauthorized"){res.writeHead(401);res.end(JSON.stringify({error:401,message:"Unauthorized"}));return}
|
||||
if(mode==="unsupported"){res.writeHead(404);res.end(JSON.stringify({error:404,message:"no such command"}));return}
|
||||
if(mode==="malformed"){res.end(JSON.stringify({containers:[]}));return}
|
||||
if(mode==="dependency"&&req.url==="/rest/container/envs"){res.writeHead(500);res.end(JSON.stringify({error:500}));return}
|
||||
res.end(JSON.stringify(req.url==="/rest/container"?[{".id":"*A",name:"native",running:"true"}]:[]))
|
||||
})
|
||||
await new Promise<void>(resolve=>router.listen(0,"127.0.0.1",resolve))
|
||||
const address=router.address();assert(address&&typeof address!=="string")
|
||||
const device={...server,host:"127.0.0.1",port:address.port,useSsl:false,verifySsl:false,enabled:true,username:"fixture",password:"fixture"} as Parameters<typeof listContainersForServer>[0]
|
||||
try{
|
||||
const before=requests;await assert.rejects(listContainersForServer({...device,enabled:false}),/выключен/);assert.equal(requests,before)
|
||||
const rows=await listContainersForServer(device);assert.equal(rows[0].status,"running");assert.equal(rows[0].startOnBoot,null)
|
||||
for(const failure of ["unauthorized","malformed","dependency"]){mode=failure;await assert.rejects(listContainersForServer(device),/Не удалось полностью/)}
|
||||
mode="unsupported";assert.deepEqual(await listContainersForServer(device),[])
|
||||
}finally{await new Promise<void>((resolve,reject)=>router.close(error=>error?reject(error):resolve()))}
|
||||
console.log("Legacy containers: real loopback HTTP handles native flags, auth/malformed/partial errors; disabled server makes zero requests; invalid IDs never query DB")
|
||||
@@ -2,7 +2,9 @@ import {boundedCompleteCount,completeContainerCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient, MikrotikError } from "./mikrotik.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { containerRuntimeStatus } from "./container-runtime-status.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
@@ -13,28 +15,41 @@ interface RosContainer {
|
||||
"remote-image"?: string
|
||||
interface?: string
|
||||
envlist?: string
|
||||
envlists?: string
|
||||
mounts?: string
|
||||
mountlists?: string
|
||||
cmd?: string
|
||||
"start-on-boot"?: string
|
||||
comment?: string
|
||||
status?: string
|
||||
"memory-high"?: string
|
||||
"memory-current"?: string
|
||||
cpu?: string
|
||||
running?: string | boolean
|
||||
stopped?: string | boolean
|
||||
error?: string | boolean
|
||||
failed?: string | boolean
|
||||
extracting?: string | boolean
|
||||
downloading?: string | boolean
|
||||
starting?: string | boolean
|
||||
stopping?: string | boolean
|
||||
}
|
||||
|
||||
interface RosContainerEnv {
|
||||
name?: string
|
||||
list?: string
|
||||
key?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface RosContainerMount {
|
||||
name?: string
|
||||
list?: string
|
||||
src?: string
|
||||
dst?: string
|
||||
}
|
||||
|
||||
export type ContainerLiveStatus = "running" | "stopped" | "error"
|
||||
export type ContainerLiveStatus = "running" | "stopped" | "error" | "unknown"
|
||||
|
||||
export type ContainerLive = {
|
||||
id: string
|
||||
@@ -48,24 +63,13 @@ export type ContainerLive = {
|
||||
mounts: { dst: string; src?: string }[]
|
||||
interfaces: string[]
|
||||
cmd?: string
|
||||
startOnBoot: boolean
|
||||
startOnBoot: boolean | null
|
||||
comment: string
|
||||
uptime?: string
|
||||
cpu?: number
|
||||
memMb?: number
|
||||
}
|
||||
|
||||
function rosYes(v: string | undefined): boolean {
|
||||
return v === "true" || v === "yes"
|
||||
}
|
||||
|
||||
function mapStatus(raw: string | undefined): ContainerLiveStatus {
|
||||
const s = (raw ?? "").toLowerCase()
|
||||
if (s === "running") return "running"
|
||||
if (s === "error" || s === "failed") return "error"
|
||||
return "stopped"
|
||||
}
|
||||
|
||||
function splitCsv(v: string | undefined): string[] {
|
||||
return (v ?? "")
|
||||
.split(",")
|
||||
@@ -82,17 +86,7 @@ function parseImageTag(c: RosContainer): { image: string; tag: string } {
|
||||
}
|
||||
return { image: remote, tag: (c.tag ?? "latest").trim() || "latest" }
|
||||
}
|
||||
return { image: (c.name ?? "").trim(), tag: (c.tag ?? "latest").trim() || "latest" }
|
||||
}
|
||||
|
||||
function isMissingPackage(err: unknown): boolean {
|
||||
if (err instanceof MikrotikError) {
|
||||
if (err.statusCode === 404) return true
|
||||
const body = err.body.toLowerCase()
|
||||
return body.includes("no such command") || body.includes("not found") || body.includes("unknown")
|
||||
}
|
||||
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase()
|
||||
return msg.includes("no such command") || msg.includes("404")
|
||||
return { image: "", tag: "" }
|
||||
}
|
||||
|
||||
export function mapContainerRow(
|
||||
@@ -105,14 +99,13 @@ export function mapContainerRow(
|
||||
const rosId = String(c[".id"] ?? `c-${idx}`)
|
||||
const name = (c.name ?? "").trim() || `container-${idx + 1}`
|
||||
const { image, tag } = parseImageTag(c)
|
||||
const envlist = (c.envlist ?? "").trim()
|
||||
const mountNames = new Set(splitCsv(c.mounts))
|
||||
const envRows = envlist
|
||||
? envs.filter((e) => (e.name ?? "").trim() === envlist && (e.key ?? "").trim())
|
||||
: []
|
||||
const mountRows = mounts.filter((m) => mountNames.has((m.name ?? "").trim()) && (m.dst ?? "").trim())
|
||||
const cpuRaw = Number.parseInt(c.cpu ?? "", 10)
|
||||
const memRaw = Number.parseInt(c["memory-high"] ?? "", 10)
|
||||
const envNames = new Set(splitCsv(c.envlists ?? c.envlist))
|
||||
const mountNames = new Set(splitCsv(c.mountlists ?? c.mounts))
|
||||
const envRows = envs.filter(e => envNames.has((e.list ?? e.name ?? "").trim()) && (e.key ?? "").trim())
|
||||
const mountRows = mounts.filter((m) => mountNames.has((m.list ?? m.name ?? "").trim()) && (m.dst ?? "").trim())
|
||||
const cpuRaw = Number(c.cpu?.trim() || NaN)
|
||||
const memRaw = Number(c["memory-current"]?.trim() || NaN)
|
||||
const boot = c["start-on-boot"]
|
||||
return {
|
||||
id: `${server.id}-${rosId}`,
|
||||
rosId,
|
||||
@@ -120,56 +113,35 @@ export function mapContainerRow(
|
||||
serverId: String(server.id),
|
||||
image,
|
||||
tag,
|
||||
status: mapStatus(c.status),
|
||||
status: containerRuntimeStatus({ ...c }),
|
||||
envs: envRows.map((e) => ({ key: e.key ?? "", value: e.value ?? "" })),
|
||||
mounts: mountRows.map((m) => ({ dst: m.dst ?? "", src: m.src || undefined })),
|
||||
interfaces: splitCsv(c.interface),
|
||||
cmd: (c.cmd ?? "").trim() || undefined,
|
||||
startOnBoot: rosYes(c["start-on-boot"]),
|
||||
startOnBoot: boot === "true" || boot === "yes" ? true : boot === "false" || boot === "no" ? false : null,
|
||||
comment: c.comment ?? "",
|
||||
cpu: Number.isFinite(cpuRaw) ? cpuRaw : undefined,
|
||||
memMb: Number.isFinite(memRaw) ? Math.round(memRaw / (1024 * 1024)) || undefined : undefined,
|
||||
cpu: Number.isFinite(cpuRaw) && cpuRaw >= 0 && cpuRaw <= 100 ? cpuRaw : undefined,
|
||||
memMb: Number.isFinite(memRaw) && memRaw >= 0 ? Math.round(memRaw / (1024 * 1024)) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContainersForServer(server: ServerRow): Promise<ContainerLive[]> {
|
||||
if (!server.enabled) throw new Error("Сервер выключен. Опрос контейнеров недоступен.")
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const [raw, envsRaw, mountsRaw] = await Promise.all([
|
||||
client.get<RosContainer[]>("/container"),
|
||||
client.get<RosContainerEnv[]>("/container/envs").catch(() => [] as RosContainerEnv[]),
|
||||
client.get<RosContainerMount[]>("/container/mounts").catch(() => [] as RosContainerMount[]),
|
||||
])
|
||||
const list = Array.isArray(raw) ? raw : []
|
||||
const envs = Array.isArray(envsRaw) ? envsRaw : []
|
||||
const mounts = Array.isArray(mountsRaw) ? mountsRaw : []
|
||||
return list.map((c, idx) => mapContainerRow(server, c, envs, mounts, idx))
|
||||
} catch (err) {
|
||||
if (isMissingPackage(err)) return []
|
||||
throw err
|
||||
}
|
||||
const { readContainerServerState } = await import("./containers-state.js")
|
||||
const state = await readContainerServerState(server, path => client.get(path))
|
||||
if (state.availability === "error" || state.failures.length) throw new Error("Не удалось полностью получить контейнеры сервера " + server.name + ": " + state.failures.map(f => f.error).join("; "))
|
||||
return state.containers
|
||||
}
|
||||
|
||||
export async function listContainers(): Promise<ContainerLive[]> {
|
||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
const results = await Promise.all(
|
||||
enabledServers.map(async (server) => {
|
||||
try {
|
||||
return await fetchContainersForServer(server)
|
||||
} catch {
|
||||
return [] as ContainerLive[]
|
||||
}
|
||||
}),
|
||||
)
|
||||
const results = await Promise.all(enabledServers.map(fetchContainersForServer))
|
||||
return results.flat()
|
||||
}
|
||||
|
||||
export async function listContainersForServer(server: ServerRow): Promise<ContainerLive[]> {
|
||||
try {
|
||||
return await fetchContainersForServer(server)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return fetchContainersForServer(server)
|
||||
}
|
||||
|
||||
export async function countContainers():Promise<number|undefined>{return boundedCompleteCount(()=>listContainersState(),completeContainerCount)}
|
||||
@@ -196,8 +168,8 @@ export async function removeContainer(server: ServerRow, rosId: string): Promise
|
||||
}
|
||||
|
||||
export async function getEnabledServerById(serverId: string | number) {
|
||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||
if (!Number.isFinite(id)) return null
|
||||
const id = parseDbServerId(serverId)
|
||||
if (id === null) return null
|
||||
return (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0] ?? null
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readContainerServerState, collectContainersState } from "./containers-s
|
||||
import { MikrotikError } from "./mikrotik.js"
|
||||
import { containersStateSchema } from "@mmapp/contracts/containers"
|
||||
|
||||
const server = { id: 1, name: "Router" } as Parameters<typeof readContainerServerState>[0]
|
||||
const server = { id: 1, name: "Router", enabled: true } as Parameters<typeof readContainerServerState>[0]
|
||||
const row = { ".id": "*A", name: "nginx", "remote-image": "nginx:alpine", interface: "veth1", envlist: "e", mounts: "m", status: "running", "start-on-boot": "yes", cpu: "0", "memory-current": "0", "memory-high": "52428800" }
|
||||
const read = async (path: string): Promise<unknown> => path === "/container" ? [row] : path === "/container/envs" ? [{name:"e",key:"A",value:"B"}] : [{name:"m",src:"/disk",dst:"/data"}]
|
||||
const good = await readContainerServerState(server, read)
|
||||
@@ -49,3 +49,8 @@ const modern=await readContainerServerState(server,async path=>path==="/containe
|
||||
|
||||
let factories=0
|
||||
const setupFailure=await collectContainersState([server,{...server,id:2}],srv=>{if(srv.id===2)throw new Error("client setup refused");factories++;return read});assert.equal(setupFailure.servers[0].containers.length,1);assert.equal(setupFailure.servers[1].availability,"error");assert.equal(setupFailure.servers[1].failures[0].error,"client setup refused");assert.equal(factories,1)
|
||||
let disabledReads=0
|
||||
const disabled=await readContainerServerState({...server,enabled:false},async()=>{disabledReads++;throw new Error("Must never poll disabled")})
|
||||
assert.equal(disabledReads,0);assert.equal(disabled.availability,"error");assert.match(disabled.failures[0].error,/выключен/)
|
||||
const enabledOnly=await collectContainersState([server,{...server,id:2,enabled:false}],srv=>{assert.equal(srv.id,1);return read})
|
||||
assert.deepEqual(enabledOnly.servers.map(s=>s.serverId),[1])
|
||||
@@ -26,6 +26,7 @@ function missingPackage(error: unknown) {
|
||||
}
|
||||
export async function readContainerServerState(server: ServerRow, read: Reader): Promise<ContainerServerState> {
|
||||
const base = { serverId: server.id, collectedAt: new Date().toISOString(), containers: [], failures: [] }
|
||||
if (!server.enabled) return containerServerStateSchema.parse({ ...base, availability: "error", failures: [{ resource: "containers", error: "Сервер выключен. Опрос контейнеров недоступен." }] })
|
||||
let raw: z.infer<typeof rawSchema>
|
||||
try { raw = rawSchema.parse(await read("/container")) }
|
||||
catch (error) {
|
||||
@@ -57,7 +58,7 @@ export async function readContainerServerState(server: ServerRow, read: Reader):
|
||||
return containerServerStateSchema.parse({ serverId: server.id, collectedAt, availability: "available", containers, failures })
|
||||
}
|
||||
export async function collectContainersState(servers: ServerRow[], makeReader: (server: ServerRow) => Reader): Promise<ContainersState> {
|
||||
const states = await Promise.all(servers.map(server => {
|
||||
const states = await Promise.all(servers.filter(server => server.enabled).map(server => {
|
||||
let reader: Reader | undefined
|
||||
return readContainerServerState(server, path => { reader ??= makeReader(server); return reader(path) })
|
||||
}))
|
||||
|
||||
Reference in new issue
Block a user