175 lines
6.8 KiB
TypeScript
175 lines
6.8 KiB
TypeScript
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
|
// Responsibility: shared WebProbe observe action configuration helpers.
|
|
import { readFileSync } from "node:fs";
|
|
import { repoRoot, rootPath } from "../config";
|
|
import { runCommand } from "../command";
|
|
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeAuthLoginSpec, type HwlabRuntimeWebProbeBrowserFreezePolicySpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
|
|
|
import type { WebProbeBrowserProxyMode } from "./entry";
|
|
import { transPath } from "./runtime-common";
|
|
import { record, shellQuote } from "./utils";
|
|
|
|
export function webObserveGcDefaultKeepHours(): number {
|
|
const configPath = "config/unidesk-cli.yaml";
|
|
const parsed = record(Bun.YAML.parse(readFileSync(rootPath(configPath), "utf8")) as unknown);
|
|
const gc = record(parsed.gc);
|
|
const scratch = record(gc.stateStaleScratch);
|
|
const keepHours = scratch.keepHours;
|
|
if (!Number.isInteger(keepHours) || keepHours < 0) {
|
|
throw new Error(`${configPath}#gc.stateStaleScratch.keepHours must be a non-negative integer`);
|
|
}
|
|
return keepHours;
|
|
}
|
|
|
|
export function nodeWebProbeAlertThresholds(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeAlertThresholdsSpec {
|
|
const thresholds = spec.webProbe?.alertThresholds;
|
|
if (thresholds === undefined) {
|
|
throw new Error(`${hwlabRuntimeLaneConfigPath()} node=${spec.nodeId} lane=${spec.lane} requires webProbe.alertThresholds for web-probe observe`);
|
|
}
|
|
return thresholds;
|
|
}
|
|
|
|
export function nodeWebProbeBrowserFreezePolicy(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeBrowserFreezePolicySpec {
|
|
const policy = spec.webProbe?.browserFreezePolicy;
|
|
if (policy === undefined) {
|
|
throw new Error(`${hwlabRuntimeLaneConfigPath()} node=${spec.nodeId} lane=${spec.lane} requires webProbe.browserFreezePolicy for web-probe observe`);
|
|
}
|
|
return policy;
|
|
}
|
|
|
|
export function nodeWebProbeProjectManagementConfig(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeProjectManagementSpec | null {
|
|
return spec.webProbe?.projectManagement ?? null;
|
|
}
|
|
|
|
export function nodeWebProbeAuthLoginConfig(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeAuthLoginSpec | null {
|
|
return spec.webProbe?.authLogin ?? null;
|
|
}
|
|
|
|
export interface NodeWebProbeHostProxyEnv {
|
|
readonly envAssignments: string[];
|
|
readonly summary: Record<string, unknown>;
|
|
}
|
|
|
|
export function nodeWebProbeHostProxyEnv(spec: HwlabRuntimeLaneSpec, browserProxyMode: WebProbeBrowserProxyMode = "auto"): NodeWebProbeHostProxyEnv {
|
|
if (browserProxyMode === "direct") {
|
|
return {
|
|
envAssignments: [],
|
|
summary: {
|
|
source: "option",
|
|
mode: "direct",
|
|
networkProfileId: spec.networkProfileId,
|
|
proxy: { enabled: false },
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
const proxy = spec.networkProfile.proxy;
|
|
const serviceCache = new Map<string, string>();
|
|
const http = resolveNodeWebProbeHostProxyUrl(spec, proxy.http, serviceCache);
|
|
const https = resolveNodeWebProbeHostProxyUrl(spec, proxy.https, serviceCache);
|
|
const all = resolveNodeWebProbeHostProxyUrl(spec, proxy.all, serviceCache);
|
|
const noProxy = proxy.noProxy.join(",");
|
|
return {
|
|
envAssignments: [
|
|
["HTTP_PROXY", http.url],
|
|
["HTTPS_PROXY", https.url],
|
|
["ALL_PROXY", all.url],
|
|
["http_proxy", http.url],
|
|
["https_proxy", https.url],
|
|
["all_proxy", all.url],
|
|
["NO_PROXY", noProxy],
|
|
["no_proxy", noProxy],
|
|
].map(([key, value]) => `${key}=${shellQuote(value)}`),
|
|
summary: {
|
|
source: "yaml",
|
|
mode: "host-env",
|
|
networkProfileId: spec.networkProfileId,
|
|
proxy: {
|
|
http: http.summary,
|
|
https: https.summary,
|
|
all: all.summary,
|
|
noProxyCount: proxy.noProxy.length,
|
|
},
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function resolveNodeWebProbeHostProxyUrl(
|
|
spec: HwlabRuntimeLaneSpec,
|
|
rawUrl: string,
|
|
serviceCache: Map<string, string>,
|
|
): { url: string; summary: Record<string, unknown> } {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(rawUrl);
|
|
} catch (error) {
|
|
throw new Error(`config/hwlab-node-lanes.yaml networkProfiles.${spec.networkProfileId}.proxy contains invalid proxy URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
const service = parseKubernetesServiceDnsHost(parsed.hostname);
|
|
if (service === null) {
|
|
return {
|
|
url: rawUrl,
|
|
summary: {
|
|
mode: "host-url",
|
|
host: parsed.hostname,
|
|
port: parsed.port || null,
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
const clusterIp = resolveKubernetesServiceClusterIp(spec, service.namespace, service.name, serviceCache);
|
|
const originalHost = parsed.hostname;
|
|
parsed.hostname = clusterIp;
|
|
const resolvedUrl = normalizedProxyUrl(parsed);
|
|
return {
|
|
url: resolvedUrl,
|
|
summary: {
|
|
mode: "k8s-service-cluster-ip",
|
|
service: service.name,
|
|
namespace: service.namespace,
|
|
originalHost,
|
|
resolvedHost: clusterIp,
|
|
port: parsed.port || null,
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function parseKubernetesServiceDnsHost(hostname: string): { name: string; namespace: string } | null {
|
|
const match = hostname.toLowerCase().match(/^([a-z0-9]([-a-z0-9]*[a-z0-9])?)\.([a-z0-9]([-a-z0-9]*[a-z0-9])?)\.svc(?:\.cluster\.local)?$/u);
|
|
if (match === null) return null;
|
|
return { name: match[1] ?? "", namespace: match[3] ?? "" };
|
|
}
|
|
|
|
export function resolveKubernetesServiceClusterIp(
|
|
spec: HwlabRuntimeLaneSpec,
|
|
namespace: string,
|
|
serviceName: string,
|
|
serviceCache: Map<string, string>,
|
|
): string {
|
|
const cacheKey = `${namespace}/${serviceName}`;
|
|
const cached = serviceCache.get(cacheKey);
|
|
if (cached !== undefined) return cached;
|
|
const result = runCommand([transPath(), spec.nodeKubeRoute, "get", "svc", "-n", namespace, serviceName, "-o", "jsonpath={.spec.clusterIP}"], repoRoot, { timeoutMs: 20_000 });
|
|
const clusterIp = result.stdout.trim();
|
|
if (result.exitCode !== 0 || clusterIp.length === 0) {
|
|
const reason = result.stderr.trim().slice(-500) || result.stdout.trim().slice(-500) || `exitCode=${result.exitCode}`;
|
|
throw new Error(`web-probe proxy service resolution failed for ${spec.nodeId}/${spec.lane} ${namespace}/${serviceName}: ${reason}`);
|
|
}
|
|
serviceCache.set(cacheKey, clusterIp);
|
|
return clusterIp;
|
|
}
|
|
|
|
export function normalizedProxyUrl(parsed: URL): string {
|
|
const value = parsed.toString();
|
|
if (parsed.pathname === "/" && parsed.search === "" && parsed.hash === "") return value.replace(/\/$/u, "");
|
|
return value;
|
|
}
|
|
|
|
export function webProbeAccountEnvAssignments(): string[] {
|
|
return Object.entries(process.env)
|
|
.filter(([key, value]) => value !== undefined && /^HWLAB_WEB_(?:ACCOUNT_)?[A-Z0-9_]+_(?:JSON|USER|PASS)$/u.test(key))
|
|
.map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
|
|
}
|