diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index 45344a0d..129d91bb 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -527,6 +527,76 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { }; } +function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", pathWithQuery: string, body: Record | null, timeoutSeconds: number): Record { + const namespace = stringAt(state.runtime, "namespace"); + const serviceName = stringAt(state.runtime, "serviceName"); + const servicePort = numberAt(state.runtime, "servicePort"); + const deploymentName = stringAt(state.runtime, "deploymentName"); + const proxyPath = `/api/v1/namespaces/${namespace}/services/${serviceName}:${servicePort}/proxy${pathWithQuery}`; + const bodyB64 = Buffer.from(body === null ? "" : JSON.stringify(body), "utf8").toString("base64"); + const pathB64 = Buffer.from(pathWithQuery, "utf8").toString("base64"); + const postScript = [ + "const path = Buffer.from(process.env.SENTINEL_PATH_B64 || '', 'base64').toString('utf8');", + "const body = Buffer.from(process.env.SENTINEL_BODY_B64 || '', 'base64').toString('utf8');", + `const url = 'http://127.0.0.1:${servicePort}' + path;`, + "fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body }).then(async (response) => {", + " const text = await response.text();", + " process.stdout.write(text);", + " if (!response.ok) process.exit(22);", + "}).catch((error) => {", + " console.error(error && error.stack ? error.stack : String(error));", + " process.exit(23);", + "});", + ].join(" "); + const script = method === "GET" + ? `kubectl get --raw ${shellQuote(proxyPath)}` + : [ + "set -eu", + `kubectl exec -n ${shellQuote(namespace)} deploy/${shellQuote(deploymentName)} -- env SENTINEL_PATH_B64=${shellQuote(pathB64)} SENTINEL_BODY_B64=${shellQuote(bodyB64)} node -e ${shellQuote(postScript)}`, + ].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; + } + return { + ok: result?.exitCode === 0, + method, + path: pathWithQuery, + internalUrl: `http://${serviceName}.${namespace}.svc.cluster.local:${servicePort}${pathWithQuery}`, + httpStatus: result?.exitCode === 0 ? 200 : null, + bodyJson: record(compactSentinelServiceBodyJson(parsed)), + 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 { + ok: value.ok, + view: value.view, + error: value.error, + availableViews: value.availableViews, + valuesRedacted: true, + }; +} + +function clipTail(value: string, maxLength: number): string { + return value.length <= maxLength ? value : value.slice(value.length - maxLength); +} + function recordQuickVerify(state: SentinelCicdState, payload: Record): Record { const views = compactQuickVerifyRecordViews(record(payload.views)); const summary = {