fix(containers): учитывать фактические состояния контейнеров
This commit is contained in:
1 parent
0cc8fe5572
commit
b3d11276dd
14 files changed
+357
-14
No files matched your search
@@ -0,0 +1,26 @@
|
||||
type Status = "running" | "stopped" | "error" | "unknown"
|
||||
const flags = ["running", "stopped", "error", "failed", "extracting", "downloading", "starting", "stopping"] as const
|
||||
/** RouterOS REST can expose active flags instead of the legacy status field. */
|
||||
export function containerRuntimeStatus(row: Record<string, unknown>): Status {
|
||||
const active = new Set<string>()
|
||||
for (const flag of flags) {
|
||||
const value = row[flag]
|
||||
if (value === undefined) continue
|
||||
if (value === true || value === "true" || value === "yes") active.add(flag)
|
||||
else if (value !== false && value !== "false" && value !== "no") return "unknown"
|
||||
}
|
||||
if (["extracting", "downloading", "starting", "stopping"].some(flag => active.has(flag))) return "unknown"
|
||||
const states: Status[] = []
|
||||
if (active.has("running")) states.push("running")
|
||||
if (active.has("stopped")) states.push("stopped")
|
||||
if (active.has("error") || active.has("failed")) states.push("error")
|
||||
if (states.length > 1) return "unknown"
|
||||
if (row.status !== undefined) {
|
||||
if (typeof row.status !== "string") return "unknown"
|
||||
const value = row.status.toLowerCase()
|
||||
const legacy = value === "running" || value === "stopped" ? value : value === "error" || value === "failed" ? "error" : "unknown"
|
||||
if (legacy === "unknown" || (states.length && states[0] !== legacy)) return "unknown"
|
||||
return legacy
|
||||
}
|
||||
return states[0] ?? "unknown"
|
||||
}
|
||||
@@ -10,8 +10,19 @@ const good = await readContainerServerState(server, read)
|
||||
assert.equal(good.availability, "available"); assert.equal(good.containers[0].status,"running")
|
||||
assert.equal(good.containers[0].cpu,0);assert.equal(good.containers[0].memMb,0);assert.equal(good.containers[0].startOnBoot,true)
|
||||
assert.deepEqual(good.containers[0].envs,[{key:"A",value:"B"}]);assert.deepEqual(good.containers[0].mounts,[{src:"/disk",dst:"/data"}])
|
||||
const nativeEnvLists = await readContainerServerState(server, async path => path === "/container" ? [{ ...row, envlist: undefined, envlists: "e" }] : read(path))
|
||||
assert.deepEqual(nativeEnvLists.containers[0].envs, [{ key: "A", value: "B" }])
|
||||
const unknown = await readContainerServerState(server, async path => path === "/container" ? [{".id":"*B",name:"pending",status:"extracting"}] : [])
|
||||
assert.equal(unknown.containers[0].status,"unknown");assert.equal(unknown.containers[0].startOnBoot,null);assert.equal(unknown.containers[0].image,"")
|
||||
for (const [flags, expected] of [
|
||||
[{ stopped: "true" }, "stopped"], [{ running: "true" }, "running"], [{ error: true }, "error"],
|
||||
[{ stopped: true, running: false }, "stopped"], [{ stopped: "true", running: "true" }, "unknown"],
|
||||
[{ stopped: "true", extracting: "true" }, "unknown"], [{ stopped: "maybe" }, "unknown"],
|
||||
[{ status: "running", stopped: "true" }, "unknown"], [{ running: "false", stopped: "false" }, "unknown"],
|
||||
] as const) {
|
||||
const result = await readContainerServerState(server, async path => path === "/container" ? [{ ".id": "*N", name: "native", ...flags }] : [])
|
||||
assert.equal(result.availability, "available"); assert.equal(result.containers[0].status, expected)
|
||||
}
|
||||
const empty = await readContainerServerState(server, async()=>[])
|
||||
assert.equal(empty.availability,"available");assert.equal(empty.containers.length,0)
|
||||
for (const raw of [{containers:[]},[{}],[row,row]]) {
|
||||
|
||||
@@ -2,14 +2,19 @@ import { z } from "zod"
|
||||
import { containersStateSchema, containerServerStateSchema, type ContainersState, type ContainerServerState } from "@mmapp/contracts/containers"
|
||||
import { mapContainerRow } from "./containers-live.js"
|
||||
import { MikrotikError } from "./mikrotik.js"
|
||||
import { containerRuntimeStatus } from "./container-runtime-status.js"
|
||||
|
||||
type ServerRow = Parameters<typeof mapContainerRow>[0]
|
||||
type Reader = (path: string) => Promise<unknown>
|
||||
const rawSchema = z.array(z.object({
|
||||
".id": z.string().min(1), name: z.string().optional(), tag: z.string().optional(), "remote-image": z.string().optional(),
|
||||
interface: z.string().optional(), envlist: z.string().optional(), mounts: z.string().optional(), mountlists: z.string().optional(),
|
||||
interface: z.string().optional(), envlist: z.string().optional(), envlists: z.string().optional(), mounts: z.string().optional(), mountlists: z.string().optional(),
|
||||
cmd: z.string().optional(), "start-on-boot": z.string().optional(), comment: z.string().optional(),
|
||||
status: z.string().optional(), "memory-high": z.string().optional(), "memory-current": z.string().optional(), cpu: z.string().optional(),
|
||||
running: z.union([z.string(), z.boolean()]).optional(), stopped: z.union([z.string(), z.boolean()]).optional(),
|
||||
error: z.union([z.string(), z.boolean()]).optional(), failed: z.union([z.string(), z.boolean()]).optional(),
|
||||
extracting: z.union([z.string(), z.boolean()]).optional(), downloading: z.union([z.string(), z.boolean()]).optional(),
|
||||
starting: z.union([z.string(), z.boolean()]).optional(), stopping: z.union([z.string(), z.boolean()]).optional(),
|
||||
})).superRefine((rows, ctx) => {
|
||||
if (new Set(rows.map(r => r[".id"])).size !== rows.length) ctx.addIssue({ code: "custom", message: "Дубли RouterOS ID" })
|
||||
})
|
||||
@@ -39,13 +44,12 @@ export async function readContainerServerState(server: ServerRow, read: Reader):
|
||||
const envRows = envs.status === "fulfilled" ? envs.value : []
|
||||
const mountRows = mounts.status === "fulfilled" ? mounts.value : []
|
||||
const mapped = mapContainerRow(server, { ...row, mounts: row.mountlists ?? row.mounts }, envRows.map(item => ({ ...item, name: item.list ?? item.name })), mountRows.map(item => ({ ...item, name: item.list ?? item.name })), index)
|
||||
const envLists = new Set((row.envlist ?? "").split(",").map(name => name.trim()).filter(Boolean))
|
||||
const envLists = new Set((row.envlists ?? row.envlist ?? "").split(",").map(name => name.trim()).filter(Boolean))
|
||||
mapped.envs = envRows.filter(item => envLists.has(item.list ?? item.name ?? "")).map(item => ({ key: item.key, value: item.value }))
|
||||
const status = (row.status ?? "").toLowerCase()
|
||||
const boot = row["start-on-boot"]
|
||||
const mem = Number(row["memory-current"]?.trim() || NaN)
|
||||
return { ...mapped, image: row["remote-image"] ? mapped.image : "", tag: row["remote-image"] ? mapped.tag : "",
|
||||
status: status === "running" || status === "stopped" ? status : status === "error" || status === "failed" ? "error" : "unknown",
|
||||
status: containerRuntimeStatus(row),
|
||||
startOnBoot: boot === "true" || boot === "yes" ? true : boot === "false" || boot === "no" ? false : null,
|
||||
cpu: mapped.cpu !== undefined && mapped.cpu >= 0 && mapped.cpu <= 100 ? mapped.cpu : undefined,
|
||||
memMb: Number.isFinite(mem) && mem >= 0 ? Math.round(mem / (1024 * 1024)) : undefined }
|
||||
|
||||
Reference in new issue
Block a user