Merge pull request #1146 from pikasTech/fix/1144-web-sentinel-service-proxy
fix: Web哨兵 internal probe 改用 Service proxy
This commit is contained in:
@@ -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}`}`;
|
||||
|
||||
Reference in New Issue
Block a user