fix: route web-probe through YAML proxy

This commit is contained in:
Codex
2026-06-21 20:46:15 +00:00
parent 625e3a1ab2
commit 56f1ed44a4
3 changed files with 244 additions and 6 deletions
+125 -4
View File
@@ -6727,9 +6727,10 @@ function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown>
if (options.action === "script") return runNodeWebProbeScript(options, spec, secretSpec, material, credential);
if (options.commandTimeoutSeconds > 55) return runNodeWebProbeAsync(options, spec, secretSpec, material, credential);
const probeArgs = nodeWebProbeRunArgs(options, "run");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const script = [
"set -eu",
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)} HWLAB_WEB_PASS=${shellQuote(material.password)} ${probeArgs.map(shellQuote).join(" ")}`,
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
@@ -6748,6 +6749,7 @@ function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown>
lane: options.lane,
workspace: spec.workspace,
url: options.url,
network: webProbeProxy.summary,
credential,
commandTimeout: {
seconds: options.commandTimeoutSeconds,
@@ -6791,9 +6793,10 @@ function runNodeWebProbeAsync(
credential: Record<string, unknown>,
): Record<string, unknown> {
const startArgs = nodeWebProbeRunArgs(options, "start");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const startScript = [
"set -eu",
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)} HWLAB_WEB_PASS=${shellQuote(material.password ?? "")} ${startArgs.map(shellQuote).join(" ")}`,
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
].join("\n");
const startResult = runTransWorkspaceStdinScript(options.node, spec.workspace, startScript, 55);
const start = parseJsonObject(startResult.stdout);
@@ -6807,6 +6810,7 @@ function runNodeWebProbeAsync(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
network: webProbeProxy.summary,
credential,
mode: "async-start",
commandTimeout: webProbeCommandTimeoutSummary(options, false),
@@ -6841,6 +6845,7 @@ function runNodeWebProbeAsync(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
network: webProbeProxy.summary,
credential,
mode: "async",
commandTimeout: webProbeCommandTimeoutSummary(options, poll.timedOut),
@@ -6947,6 +6952,116 @@ function webProbeCredential(secretSpec: RuntimeSecretSpec, material: BootstrapAd
};
}
interface NodeWebProbeHostProxyEnv {
readonly envAssignments: string[];
readonly summary: Record<string, unknown>;
}
function nodeWebProbeHostProxyEnv(spec: HwlabRuntimeLaneSpec): NodeWebProbeHostProxyEnv {
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,
},
};
}
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,
},
};
}
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] ?? "" };
}
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;
}
function normalizedProxyUrl(parsed: URL): string {
const value = parsed.toString();
if (parsed.pathname === "/" && parsed.search === "" && parsed.hash === "") return value.replace(/\/$/u, "");
return value;
}
function runNodeWebProbeObserve(
options: NodeWebProbeObserveOptions,
spec: HwlabRuntimeLaneSpec,
@@ -6977,6 +7092,7 @@ function runNodeWebProbeObserveStart(
const day = timestamp.slice(0, 8);
const stateDir = `.state/web-observe/${safeWebObserveSegment(options.node)}/${safeWebObserveSegment(options.lane)}/${day.slice(0, 4)}/${day.slice(4, 6)}/${day.slice(6, 8)}/${timestamp}_${safeWebObserveTargetSegment(options.targetPath)}_${jobId}`;
const runnerB64 = Buffer.from(nodeWebObserveRunnerSource(), "utf8").toString("base64");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const script = [
"set -eu",
`state_dir=${shellQuote(stateDir)}`,
@@ -6986,6 +7102,7 @@ function runNodeWebProbeObserveStart(
`node -e "require('fs').writeFileSync(process.argv[1], Buffer.from(process.argv[2], 'base64'))" "$runner" ${shellQuote(runnerB64)}`,
"chmod 700 \"$runner\"",
[
...webProbeProxy.envAssignments,
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
@@ -7029,6 +7146,7 @@ function runNodeWebProbeObserveStart(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
network: webProbeProxy.summary,
targetPath: options.targetPath,
id: observerId,
credential,
@@ -8172,7 +8290,8 @@ function runNodeWebProbeScript(
material: BootstrapAdminPasswordMaterial,
credential: Record<string, unknown>,
): Record<string, unknown> {
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "", webProbeProxy);
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const stdoutReport = parseJsonObject(result.stdout);
const runPaths = webProbeScriptRunPathsFromStderr(result.stderr);
@@ -8250,6 +8369,7 @@ function runNodeWebProbeScript(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
network: webProbeProxy.summary,
credential,
scriptSource: options.scriptSource,
degradedReason,
@@ -8274,7 +8394,7 @@ function runNodeWebProbeScript(
});
}
function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, password: string): string {
function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, password: string, webProbeProxy: NodeWebProbeHostProxyEnv): string {
const userScriptB64 = Buffer.from(options.scriptText, "utf8").toString("base64");
const runnerB64 = Buffer.from(nodeWebProbeScriptRunnerSource(), "utf8").toString("base64");
return [
@@ -8291,6 +8411,7 @@ function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secre
`node -e "require('fs').writeFileSync(process.argv[1], Buffer.from(process.argv[2], 'base64'))" "$user_script" ${shellQuote(userScriptB64)}`,
`node -e "require('fs').writeFileSync(process.argv[1], Buffer.from(process.argv[2], 'base64'))" "$runner" ${shellQuote(runnerB64)}`,
[
...webProbeProxy.envAssignments,
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_PASS=${shellQuote(password)}`,