From 5e779cb90b717256ec5e805aac7d372932e0d74a Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 10:29:09 +0000 Subject: [PATCH] fix: use service proxy for sentinel probes --- scripts/src/hwlab-node-web-sentinel-cicd.ts | 92 +++++++++++++-------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 2ebedd18..2a058c0b 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2911,53 +2911,75 @@ function boundQuickVerifyRecordText(value: unknown, maxChars: number): string | function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", pathWithQuery: string, body: Record | null, timeoutSeconds: number): Record { 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[] = []; + let result: CommandResult | null = null; + let parsed: Record | 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 | 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, keys: readonly string[]): Record { + const picked: Record = {}; + 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 { const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, ""); const url = `${publicBaseUrl}${pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`}`;