Merge pull request #1146 from pikasTech/fix/1144-web-sentinel-service-proxy

fix: Web哨兵 internal probe 改用 Service proxy
This commit is contained in:
Lyon
2026-06-27 18:29:59 +08:00
committed by GitHub
+57 -35
View File
@@ -2911,53 +2911,75 @@ function boundQuickVerifyRecordText(value: unknown, maxChars: number): string |
function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", pathWithQuery: string, body: Record<string, unknown> | null, timeoutSeconds: number): Record<string, unknown> {
const namespace = stringAt(state.runtime, "namespace");
const deploymentName = stringAt(state.runtime, "deploymentName");
const serviceName = stringAt(state.runtime, "serviceName");
const servicePort = numberAt(state.runtime, "servicePort");
const url = `http://${serviceName}.${namespace}.svc.cluster.local:${servicePort}${pathWithQuery}`;
const proxyPath = `/api/v1/namespaces/${namespace}/services/${serviceName}:${servicePort}/proxy${pathWithQuery}`;
const bodyB64 = Buffer.from(body === null ? "" : JSON.stringify(body), "utf8").toString("base64");
const js = [
"const method=process.env.REQ_METHOD||'GET';",
"const url=process.env.REQ_URL||'';",
"const body=Buffer.from(process.env.REQ_BODY_B64||'', 'base64').toString('utf8');",
"const init={method,headers:{}};",
"if(method!=='GET'&&method!=='HEAD'){init.headers['content-type']='application/json';init.body=body;}",
"let out;",
"function rec(v){return v&&typeof v==='object'&&!Array.isArray(v)?v:{}}",
"function pick(o,keys){const r={};for(const k of keys){if(o&&Object.prototype.hasOwnProperty.call(o,k))r[k]=o[k];}return r}",
"function compactBodyJson(v){const o=rec(v);if(typeof o.renderedText!=='string')return v;return {...pick(o,['ok','view','error','availableViews','valuesRedacted']),run:pick(rec(o.run),['id','scenario_id','status','node','lane','observer_id','state_dir','report_json_sha256','finding_count','artifact_count','maintenance','created_at','updated_at']),summary:pick(rec(o.summary),['reason','status','valuesRedacted']),findings:Array.isArray(o.findings)?o.findings.slice(0,12):[],renderedText:o.renderedText,valuesRedacted:true}}",
"try{const res=await fetch(url,init);const text=await res.text();let bodyJson=null;try{bodyJson=JSON.parse(text)}catch{};out={ok:res.ok,httpStatus:res.status,contentType:res.headers.get('content-type'),bodyJson:compactBodyJson(bodyJson),bodyTextPreview:bodyJson===null?text.slice(0,4000):'',bodyBytes:Buffer.byteLength(text),valuesRedacted:true};}",
"catch(error){out={ok:false,error:error instanceof Error?error.message:String(error),valuesRedacted:true};}",
"console.log(JSON.stringify(out));",
].join("");
const script = [
"set +e",
`namespace=${shellQuote(namespace)}`,
`deployment=${shellQuote(deploymentName)}`,
`method=${shellQuote(method)}`,
`url=${shellQuote(url)}`,
`body_b64=${shellQuote(bodyB64)}`,
`js=${shellQuote(js)}`,
"if ! kubectl -n \"$namespace\" get deploy \"$deployment\" >/dev/null 2>&1; then node - \"$namespace\" \"$deployment\" <<'NODE'\nconst [namespace,deployment]=process.argv.slice(2); console.log(JSON.stringify({ok:false,error:'sentinel-deployment-missing',namespace,deployment,valuesRedacted:true}));\nNODE\nexit 0; fi",
"kubectl -n \"$namespace\" exec deploy/\"$deployment\" -- env REQ_METHOD=\"$method\" REQ_URL=\"$url\" REQ_BODY_B64=\"$body_b64\" bun -e \"$js\"",
].join("\n");
const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 });
const parsed = parseJsonObject(result.stdout);
const script = method === "GET"
? `kubectl get --raw ${shellQuote(proxyPath)}`
: [
"set -eu",
`proxy_path=${shellQuote(proxyPath)}`,
`body_b64=${shellQuote(bodyB64)}`,
"body_file=$(mktemp \"${TMPDIR:-/tmp}/sentinel-service-proxy.body.XXXXXX\")",
"trap 'rm -f \"$body_file\"' EXIT",
"printf %s \"$body_b64\" | base64 -d >\"$body_file\"",
"kubectl create --raw \"$proxy_path\" -f \"$body_file\"",
].join("\n");
const maxAttempts = method === "GET" ? 3 : 1;
const attemptTimeoutSeconds = Math.max(5, Math.min(timeoutSeconds, method === "GET" ? 15 : 60));
const attempts: Record<string, unknown>[] = [];
let result: CommandResult | null = null;
let parsed: Record<string, unknown> | null = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
parsed = parseJsonObject(result.stdout);
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, valuesRedacted: true });
if (result.exitCode === 0) break;
}
const compactBodyJson = compactSentinelServiceBodyJson(parsed);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
ok: result?.exitCode === 0,
method,
path: pathWithQuery,
internalUrl: `http://${serviceName}.${namespace}.svc.cluster.local:${servicePort}${pathWithQuery}`,
httpStatus: parsed?.httpStatus ?? null,
bodyJson: record(parsed?.bodyJson),
bodyTextPreview: typeof parsed?.bodyTextPreview === "string" ? parsed.bodyTextPreview : "",
bodyBytes: parsed?.bodyBytes ?? null,
error: parsed?.error ?? null,
result: compactCommand(result),
httpStatus: result?.exitCode === 0 ? 200 : null,
bodyJson: record(compactBodyJson),
bodyTextPreview: parsed === null ? clipTail(result?.stdout ?? "", 4000) : "",
bodyBytes: Buffer.byteLength(result?.stdout ?? ""),
error: result?.exitCode === 0 ? null : clipTail(`${result?.stderr ?? ""}${result?.stdout ?? ""}`, 1000),
proxyPath,
result: result === null ? null : compactCommand(result),
attempts,
valuesRedacted: true,
};
}
function compactSentinelServiceBodyJson(value: Record<string, unknown> | null): unknown {
if (value === null || typeof value.renderedText !== "string") return value;
return {
...pickFields(value, ["ok", "view", "error", "availableViews", "valuesRedacted"]),
run: pickFields(record(value.run), ["id", "scenario_id", "status", "node", "lane", "observer_id", "state_dir", "report_json_sha256", "finding_count", "artifact_count", "maintenance", "created_at", "updated_at"]),
summary: pickFields(record(value.summary), ["reason", "status", "valuesRedacted"]),
findings: Array.isArray(value.findings) ? value.findings.slice(0, 12) : [],
renderedText: value.renderedText,
valuesRedacted: true,
};
}
function pickFields(value: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {
const picked: Record<string, unknown> = {};
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(value, key)) picked[key] = value[key];
}
return picked;
}
function clipTail(value: string, maxChars: number): string {
return value.length <= maxChars ? value : value.slice(-maxChars);
}
function probePublicSentinelService(state: SentinelCicdState, pathWithQuery: string, timeoutSeconds: number): Record<string, unknown> {
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
const url = `${publicBaseUrl}${pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`}`;