Stabilize provider HTTP tunnel diagnostics

This commit is contained in:
Codex
2026-05-17 06:53:03 +00:00
parent aaf6e74aa4
commit bf364baac8
12 changed files with 518 additions and 50 deletions
@@ -723,6 +723,30 @@ function parseCurlHeaderBody(output: Buffer): { status: number; contentType: str
return { status: Number.isFinite(status) ? status : 502, contentType, bodyText };
}
function bodyPreview(bodyText: string, contentType: string): JsonValue {
if (contentType.toLowerCase().includes("json")) {
try {
return JSON.parse(bodyText) as JsonValue;
} catch {
return bodyText.slice(0, 2000);
}
}
return bodyText.slice(0, 2000);
}
function responseProbeRecord(response: Response, bodyText: string, startedAt: number): JsonRecord {
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
return {
ok: response.ok,
status: response.ok ? "healthy" : "unhealthy",
upstreamStatus: response.status,
contentType,
proxyMode: response.headers.get("x-unidesk-proxy-mode") ?? "",
durationMs: Date.now() - startedAt,
body: bodyPreview(bodyText, contentType),
};
}
async function kubeApiServiceProxyResponse(
service: ManagedService,
req: Request,
@@ -733,6 +757,25 @@ async function kubeApiServiceProxyResponse(
return kubeApiProxyResponse(service, req, serviceProxyApiPath(service, targetPath), query, timeoutMs);
}
async function kubeApiServiceProxyProbe(service: ManagedService, targetPath: string, timeoutMs: number): Promise<JsonRecord> {
const startedAt = Date.now();
try {
const request = new Request("http://k3sctl-adapter.local/diagnostics", { method: "GET", headers: { accept: "application/json" } });
const response = await kubeApiServiceProxyResponse(service, request, targetPath, "", timeoutMs);
const bodyText = await response.text();
return responseProbeRecord(response, bodyText, startedAt);
} catch (error) {
return {
ok: false,
status: "unhealthy",
upstreamStatus: null,
proxyMode: "kubernetes-api-service-proxy",
durationMs: Date.now() - startedAt,
error: errorToJson(error),
};
}
}
async function nativeServiceProxyResponse(
service: ManagedService,
req: Request,
@@ -1116,6 +1159,74 @@ async function controlPlaneSnapshot(): Promise<JsonRecord> {
};
}
async function serviceDiagnostics(service: ManagedService): Promise<JsonRecord> {
const checkedAt = new Date().toISOString();
const healthPath = activeEndpoint(service).healthPath;
const healthTimeoutMs = Math.max(500, Math.min(config.healthTimeoutMs, 5000));
const kubernetesApiServiceProxy = await kubeApiServiceProxyProbe(service, healthPath, healthTimeoutMs);
const targetServiceStartedAt = Date.now();
let targetService: JsonRecord;
try {
const nativeRequest = new Request("http://k3sctl-adapter.local/diagnostics", { method: "GET", headers: { accept: "application/json" } });
const native = await nativeServiceProxyResponse(service, nativeRequest.clone(), healthPath, "", healthTimeoutMs);
const response = native ?? await kubeApiServiceProxyResponse(service, nativeRequest, healthPath, "", healthTimeoutMs);
const bodyText = await response.text();
targetService = responseProbeRecord(response, bodyText, targetServiceStartedAt);
} catch (error) {
targetService = {
ok: false,
status: "unhealthy",
upstreamStatus: null,
proxyMode: "k3sctl-service-health",
durationMs: Date.now() - targetServiceStartedAt,
error: errorToJson(error),
};
}
const managedService = await serviceStatus(service).then((status) => ({
ok: status.healthy === true,
status: String(status.status ?? ""),
servingHealthy: status.servingHealthy === true,
topologyComplete: status.topologyComplete === true,
activeInstanceId: String(status.activeInstanceId ?? ""),
active: status.active ?? null,
missingNodeIds: Array.isArray(status.missingNodeIds) ? status.missingNodeIds as JsonValue : [],
} satisfies JsonRecord)).catch((error) => ({
ok: false,
status: "unhealthy",
error: errorToJson(error),
} satisfies JsonRecord));
const kubernetesApiServiceProxyOk = kubernetesApiServiceProxy.ok === true;
const targetServiceOk = targetService.ok === true;
const checks = {
k3sctlAdapter: {
ok: true,
nodeId: config.nodeId,
clusterId: config.clusterId,
startedAt,
},
kubernetesApiServiceProxy: {
...kubernetesApiServiceProxy,
configured: kubeClient !== null,
kubeconfigPath: config.kubeconfigPath,
connectHost: config.kubeApiConnectHost,
},
targetService,
managedService,
} satisfies Record<string, JsonValue>;
const ok = kubernetesApiServiceProxyOk && targetServiceOk;
return {
ok,
service: "k3sctl-adapter",
serviceId: service.id,
namespace: service.namespace,
checkedAt,
healthPath,
route: service.route,
noFallback: true,
checks,
};
}
function forwardHeaders(request: Request): Headers {
const headers = new Headers();
for (const name of ["accept", "content-type", "x-requested-with"]) {
@@ -1165,6 +1276,13 @@ async function route(req: Request): Promise<Response> {
const status = await serviceStatus(service);
return req.method === "HEAD" ? new Response(null, { status: status.healthy === true ? 200 : 503 }) : jsonResponse({ ok: status.healthy === true, managedService: status }, status.healthy === true ? 200 : 503);
}
const diagnosticsMatch = url.pathname.match(/^\/api\/services\/([^/]+)\/diagnostics$/u);
if (diagnosticsMatch !== null && (req.method === "GET" || req.method === "HEAD")) {
const service = serviceById(decodeURIComponent(diagnosticsMatch[1] ?? ""));
if (service === null) return jsonResponse({ ok: false, error: "managed service not found" }, 404);
const diagnostics = await serviceDiagnostics(service);
return req.method === "HEAD" ? new Response(null, { status: diagnostics.ok === true ? 200 : 503 }) : jsonResponse(diagnostics, diagnostics.ok === true ? 200 : 503);
}
const proxyMatch = url.pathname.match(/^\/api\/services\/([^/]+)\/proxy(\/.*)$/u);
if (proxyMatch !== null) {
const service = serviceById(decodeURIComponent(proxyMatch[1] ?? ""));