feat: add resource monitoring and provider upgrade
This commit is contained in:
+110
-2
@@ -71,9 +71,13 @@ function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: str
|
||||
return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode };
|
||||
}
|
||||
|
||||
function dockerCoreJson(path: string): unknown {
|
||||
function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown {
|
||||
const code = `
|
||||
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)});
|
||||
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
|
||||
method: init?.method ?? "GET",
|
||||
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
|
||||
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
|
||||
})});
|
||||
const text = await res.text();
|
||||
let body = null;
|
||||
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
|
||||
@@ -88,6 +92,20 @@ function dockerCoreJson(path: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForTaskStatus(taskId: string, expected: string, timeoutMs = 10_000): Promise<unknown> {
|
||||
const started = Date.now();
|
||||
let latest: unknown = null;
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
latest = dockerCoreJson("/api/tasks?limit=40");
|
||||
const tasks = (latest as { body?: { tasks?: Array<{ id?: string; status?: string; result?: unknown }> } }).body?.tasks ?? [];
|
||||
const task = tasks.find((item) => item.id === taskId);
|
||||
if (task?.status === expected) return { ok: true, task };
|
||||
if (task?.status === "failed") return { ok: false, task };
|
||||
await Bun.sleep(500);
|
||||
}
|
||||
return { ok: false, timeoutMs, latest };
|
||||
}
|
||||
|
||||
function dockerPortSummary(): unknown {
|
||||
const result = runCommand([
|
||||
"docker",
|
||||
@@ -108,6 +126,54 @@ function dockerPortSummary(): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown {
|
||||
const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string }> } }> } };
|
||||
const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId);
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
providerId,
|
||||
nodeStatus: item?.nodeStatus,
|
||||
updatedAt: item?.updatedAt,
|
||||
dockerStatus: item?.dockerStatus === undefined ? null : {
|
||||
ok: item.dockerStatus.ok,
|
||||
socketPresent: item.dockerStatus.socketPresent,
|
||||
collectedAt: item.dockerStatus.collectedAt,
|
||||
counts: item.dockerStatus.counts,
|
||||
daemon: item.dockerStatus.daemon,
|
||||
containersPreview: (item.dockerStatus.containers ?? []).slice(0, 8).map((container) => ({
|
||||
id: container.id,
|
||||
name: container.name,
|
||||
image: container.image,
|
||||
state: container.state,
|
||||
status: container.status,
|
||||
ports: container.ports,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function systemStatusCheckDetail(systemStatus: unknown, providerId: string): unknown {
|
||||
const response = systemStatus as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; current?: { ok?: boolean; collectedAt?: string; cpu?: unknown; memory?: unknown; disk?: unknown }; history?: unknown[] }> } };
|
||||
const item = response.body?.systemStatuses?.find((entry) => entry.providerId === providerId);
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
providerId,
|
||||
nodeStatus: item?.nodeStatus,
|
||||
updatedAt: item?.updatedAt,
|
||||
current: item?.current === undefined ? null : {
|
||||
ok: item.current.ok,
|
||||
collectedAt: item.current.collectedAt,
|
||||
cpu: item.current.cpu,
|
||||
memory: item.current.memory,
|
||||
disk: item.current.disk,
|
||||
},
|
||||
historyCount: item?.history?.length ?? 0,
|
||||
historyPreview: (item?.history ?? []).slice(-8),
|
||||
};
|
||||
}
|
||||
|
||||
async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
const portSummary = dockerPortSummary() as { rows?: Array<{ name: string; ports: string }> };
|
||||
const portsText = (portSummary.rows ?? []).map((row) => `${row.name} ${row.ports}`).join("\n");
|
||||
@@ -137,11 +203,27 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
|
||||
async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
const coreOverview = dockerCoreJson("/api/overview");
|
||||
const coreNodes = dockerCoreJson("/api/nodes");
|
||||
const systemStatus = dockerCoreJson("/api/nodes/system-status?limit=24");
|
||||
const dockerStatus = dockerCoreJson("/api/nodes/docker-status");
|
||||
const providerIngress = await fetchProbe(urls.providerIngressHealthUrl);
|
||||
const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number } }).body;
|
||||
const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string }> } }).body?.nodes ?? [];
|
||||
const systemStatuses = (systemStatus as { body?: { systemStatuses?: Array<{ providerId?: string; current?: { cpu?: { percent?: number }; memory?: { percent?: number }; disk?: { percent?: number } }; history?: unknown[] }> } }).body?.systemStatuses ?? [];
|
||||
const mainSystem = systemStatuses.find((item) => item.providerId === config.providerGateway.id);
|
||||
const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? [];
|
||||
const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id);
|
||||
addCheck(checks, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview);
|
||||
addCheck(checks, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes);
|
||||
addCheck(checks, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id));
|
||||
addCheck(checks, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id));
|
||||
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
|
||||
method: "POST",
|
||||
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
|
||||
});
|
||||
const upgradeTaskId = (upgradeDispatch as { body?: { taskId?: string } }).body?.taskId ?? "";
|
||||
const upgradeTask = upgradeTaskId ? await waitForTaskStatus(upgradeTaskId, "succeeded") : { ok: false, error: "missing taskId", upgradeDispatch };
|
||||
const taskResult = (upgradeTask as { task?: { result?: { plan?: unknown; mode?: string } }; ok?: boolean }).task?.result;
|
||||
addCheck(checks, "provider:upgrade-plan", (upgradeDispatch as { ok?: boolean }).ok === true && (upgradeTask as { ok?: boolean }).ok === true && taskResult?.mode === "plan" && taskResult.plan !== undefined, { upgradeDispatch, upgradeTask });
|
||||
addCheck(checks, "provider-ingress:public-health", (providerIngress as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (providerIngress as { body?: { ok?: boolean } }).body?.ok === true, providerIngress);
|
||||
}
|
||||
|
||||
@@ -192,9 +274,35 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
await page.getByTestId(`raw-node-${config.providerGateway.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`).click();
|
||||
await page.waitForSelector('[data-testid="raw-json"]', { timeout: 5000 });
|
||||
const rawText = await page.locator('[data-testid="raw-json"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: "关闭" }).click();
|
||||
await page.getByRole("button", { name: /资源节点/ }).click();
|
||||
await page.getByRole("button", { name: /资源监控/ }).click();
|
||||
await page.waitForSelector('[data-testid="node-monitor-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-cpu"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-memory"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-disk"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText.toLowerCase();
|
||||
return text.includes("任务管理器视图") && text.includes("cpu") && text.includes("memory") && text.includes("disk");
|
||||
}, undefined, { timeout: 10000 });
|
||||
const monitorText = await page.locator('[data-testid="node-monitor-page"]').innerText({ timeout: 5000 });
|
||||
await page.getByTestId("upgrade-plan-button").click();
|
||||
await page.waitForFunction(() => document.body.innerText.includes("预检升级 已下发"), undefined, { timeout: 10000 });
|
||||
const upgradeControlText = await page.locator('[data-testid="provider-upgrade-control"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: /Docker 状态/ }).click();
|
||||
await page.waitForSelector('[data-testid="docker-status-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="docker-container-table"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText.toLowerCase();
|
||||
return text.includes("docker desktop 视图") && text.includes("containers");
|
||||
}, undefined, { timeout: 10000 });
|
||||
const dockerText = await page.locator('[data-testid="docker-status-page"]').innerText({ timeout: 5000 });
|
||||
addCheck(checks, "frontend:login-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && bodyText.includes("核心在线"), { screenshotPath });
|
||||
addCheck(checks, "frontend:no-naked-json-before-click", rawBlocksBefore === 0 && !nakedJsonText, { rawBlocksBefore, nakedJsonText });
|
||||
addCheck(checks, "frontend:raw-json-explicit-button", rawText.includes('"providerId"') && rawText.includes(config.providerGateway.id), { rawTextPreview: rawText.slice(0, 400) });
|
||||
addCheck(checks, "frontend:system-monitor-visible", monitorText.includes("任务管理器视图") && monitorText.includes("CPU") && monitorText.includes("Memory") && monitorText.includes("Disk"), { monitorTextPreview: monitorText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:upgrade-plan-dispatch", upgradeControlText.includes("预检升级 已下发"), { providerId: config.providerGateway.id, upgradeControlPreview: upgradeControlText.slice(0, 500) });
|
||||
addCheck(checks, "frontend:docker-status-visible", dockerText.toLowerCase().includes("docker desktop 视图") && dockerText.toLowerCase().includes("containers") && (dockerText.includes("unidesk-frontend") || dockerText.includes("unidesk-backend-core")), { dockerTextPreview: dockerText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors });
|
||||
return { screenshotPath, bodyText, consoleErrors };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user