fix: isolate provider egress tunnel diagnostics
This commit is contained in:
+10
-1
@@ -1,7 +1,7 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// UniDesk CLI dispatcher with bounded server lifecycle and job drill-down output.
|
||||
import { readConfig } from "./src/config";
|
||||
import { debugDispatch, debugHealth, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
||||
import { debugDispatch, debugEgressProxy, debugHealth, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
||||
import { isRebuildableService, isRestartableService, rebuildService, restartService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService, unsupportedRestartService } from "./src/docker";
|
||||
import { emitError, emitJson, emitText, isRenderedCliResult } from "./src/output";
|
||||
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, renderJobLaunchSummary, renderJobStatusSummary, runJob } from "./src/jobs";
|
||||
@@ -650,6 +650,15 @@ async function main(): Promise<void> {
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (sub === "egress-proxy") {
|
||||
const providerId = third ?? "";
|
||||
if (providerId.length === 0) throw new Error("debug egress-proxy requires providerId");
|
||||
const result = await debugEgressProxy(config, providerId);
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (sub === "dispatch") {
|
||||
const providerId = isDebugDispatchCommand(third) ? config.providerGateway.id : third ?? config.providerGateway.id;
|
||||
const commandArg = isDebugDispatchCommand(third) ? third : fourth;
|
||||
|
||||
@@ -199,6 +199,91 @@ export async function debugSshPool(_config: UniDeskConfig, providerId: string):
|
||||
};
|
||||
}
|
||||
|
||||
export async function debugEgressProxy(_config: UniDeskConfig, providerId: string): Promise<unknown> {
|
||||
const nodesResponse = await coreInternalFetch("/api/nodes");
|
||||
const body = recordValue(recordValue(nodesResponse).body);
|
||||
const nodes = arrayValue(body.nodes);
|
||||
const node = nodes
|
||||
.map((item) => recordValue(item))
|
||||
.find((item) => item.providerId === providerId) ?? null;
|
||||
if (node === null) {
|
||||
return {
|
||||
ok: false,
|
||||
providerId,
|
||||
degradedReason: "provider-not-found",
|
||||
nodesFetch: nodesResponse,
|
||||
next: { fullHealth: "bun scripts/cli.ts debug health" },
|
||||
};
|
||||
}
|
||||
const labels = recordValue(node.labels);
|
||||
const active = Number(labels.providerGatewayEgressProxyActiveTunnels ?? 0);
|
||||
const pending = Number(labels.providerGatewayEgressProxyPendingTunnels ?? 0);
|
||||
const opened = Number(labels.providerGatewayEgressProxyOpenedTunnels ?? 0);
|
||||
const stale = Number(labels.providerGatewayEgressProxyStaleTunnels ?? 0);
|
||||
const enabled = labels.providerGatewayEgressProxy === true;
|
||||
const connected = labels.providerGatewayEgressProxyConnected === true;
|
||||
const sshReady = Number(labels.providerGatewaySshDataPoolReady ?? 0);
|
||||
const activeTunnelDetails = arrayValue(labels.providerGatewayEgressProxyActiveTunnelDetails);
|
||||
const recentClosedTunnels = arrayValue(labels.providerGatewayEgressProxyRecentClosedTunnels);
|
||||
const ok = enabled && connected && stale === 0;
|
||||
return {
|
||||
ok,
|
||||
providerId,
|
||||
node: {
|
||||
providerId: node.providerId,
|
||||
name: node.name,
|
||||
status: node.status,
|
||||
lastHeartbeat: node.lastHeartbeat ?? null,
|
||||
updatedAt: node.updatedAt ?? null,
|
||||
},
|
||||
egressProxy: {
|
||||
enabled,
|
||||
connected,
|
||||
port: labels.providerGatewayEgressProxyPort ?? null,
|
||||
activeTunnels: Number.isFinite(active) ? active : 0,
|
||||
pendingTunnels: Number.isFinite(pending) ? pending : 0,
|
||||
openedTunnels: Number.isFinite(opened) ? opened : 0,
|
||||
staleTunnels: Number.isFinite(stale) ? stale : 0,
|
||||
oldestTunnelAgeMs: labels.providerGatewayEgressProxyOldestTunnelAgeMs ?? null,
|
||||
policy: {
|
||||
openTimeoutMs: labels.providerGatewayEgressProxyOpenTimeoutMs ?? null,
|
||||
idleTimeoutMs: labels.providerGatewayEgressProxyIdleTimeoutMs ?? null,
|
||||
maxTunnelAgeMs: labels.providerGatewayEgressProxyMaxTunnelAgeMs ?? null,
|
||||
staleTunnelIdleMs: labels.providerGatewayEgressProxyStaleTunnelIdleMs ?? null,
|
||||
maxPendingBytes: labels.providerGatewayEgressProxyMaxPendingBytes ?? null,
|
||||
},
|
||||
activeTunnelDetails,
|
||||
activeTunnelDetailsCount: activeTunnelDetails.length,
|
||||
recentClosedTunnels,
|
||||
recentClosedTunnelCount: recentClosedTunnels.length,
|
||||
},
|
||||
controlPlaneSeparation: {
|
||||
sshDataTransport: stringValue(labels.providerGatewaySshDataTransport),
|
||||
sshDataPoolReady: Number.isFinite(sshReady) ? sshReady : 0,
|
||||
sshDataPoolDesired: labels.providerGatewaySshDataPoolDesired ?? null,
|
||||
sshDataPoolClaimed: labels.providerGatewaySshDataPoolClaimed ?? null,
|
||||
note: "SSH/control readiness is reported separately from egress download tunnel activity.",
|
||||
},
|
||||
classification: !enabled
|
||||
? "egress-proxy-disabled"
|
||||
: !connected
|
||||
? "egress-proxy-core-channel-disconnected"
|
||||
: stale > 0
|
||||
? "egress-tunnel-stale"
|
||||
: active > 0
|
||||
? "egress-tunnel-active"
|
||||
: "egress-proxy-ready",
|
||||
outputPolicy: {
|
||||
redaction: "activeTunnelDetails expose target host/port and path/query-key summary only; URL credentials, headers, tokens and query values are not emitted.",
|
||||
boundedRecentClosedTunnels: true,
|
||||
},
|
||||
next: {
|
||||
smoke: `trans ${providerId} argv true`,
|
||||
fullHealth: "bun scripts/cli.ts debug health",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForTask(taskId: string, timeoutMs: number): Promise<unknown> {
|
||||
const started = Date.now();
|
||||
let latest: unknown = null;
|
||||
|
||||
+4
-2
@@ -95,6 +95,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "job cancel <jobId>", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." },
|
||||
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
|
||||
{ command: "debug ssh-pool <providerId>", description: "Show bounded host.ssh.tcp-pool labels for one provider, including ready/claimed/desired/lastError." },
|
||||
{ command: "debug egress-proxy <providerId>", description: "Show provider-gateway egress proxy tunnel counts, stale tunnel diagnosis, active target summaries, and recent closed tunnel lifecycle without URL credential leakage." },
|
||||
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
|
||||
{ command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." },
|
||||
@@ -539,15 +540,16 @@ function jobHelp(): unknown {
|
||||
|
||||
function debugHelp(): unknown {
|
||||
return {
|
||||
command: "debug health|ssh-pool|dispatch|task",
|
||||
command: "debug health|ssh-pool|egress-proxy|dispatch|task",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts debug health",
|
||||
"bun scripts/cli.ts debug ssh-pool <providerId>",
|
||||
"bun scripts/cli.ts debug egress-proxy <providerId>",
|
||||
"bun scripts/cli.ts debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]",
|
||||
"bun scripts/cli.ts debug task <taskId|latest>",
|
||||
],
|
||||
description: "Debug the real core/provider/dispatch paths. ssh-pool returns bounded host.ssh.tcp-pool labels for one provider; do not use debug commands as formal TEST.md acceptance steps.",
|
||||
description: "Debug the real core/provider/dispatch paths. ssh-pool and egress-proxy are separate so SSH/control readiness is not conflated with long download tunnel activity; do not use debug commands as formal TEST.md acceptance steps.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,20 @@ function providerGatewaySignal(debug: unknown, providerId: string): ProviderTria
|
||||
providerGatewayVersion: labels.providerGatewayVersion ?? null,
|
||||
hostSshConfigured: labels.hostSshConfigured ?? null,
|
||||
hostSshKeyPresent: labels.hostSshKeyPresent ?? null,
|
||||
sshDataPool: {
|
||||
transport: labels.providerGatewaySshDataTransport ?? null,
|
||||
desired: labels.providerGatewaySshDataPoolDesired ?? null,
|
||||
ready: labels.providerGatewaySshDataPoolReady ?? null,
|
||||
claimed: labels.providerGatewaySshDataPoolClaimed ?? null,
|
||||
},
|
||||
egressProxy: {
|
||||
enabled: labels.providerGatewayEgressProxy ?? null,
|
||||
connected: labels.providerGatewayEgressProxyConnected ?? null,
|
||||
activeTunnels: labels.providerGatewayEgressProxyActiveTunnels ?? null,
|
||||
pendingTunnels: labels.providerGatewayEgressProxyPendingTunnels ?? null,
|
||||
staleTunnels: labels.providerGatewayEgressProxyStaleTunnels ?? null,
|
||||
oldestTunnelAgeMs: labels.providerGatewayEgressProxyOldestTunnelAgeMs ?? null,
|
||||
},
|
||||
capabilities,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user