fix: route decision CLI to NC01 k8s
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success

This commit is contained in:
Codex
2026-07-09 20:13:40 +02:00
parent c1f0a2ac6b
commit c4ee6bcc90
3 changed files with 121 additions and 30 deletions
+102 -29
View File
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { type UniDeskConfig, repoRoot } from "./config";
import { coreInternalFetch } from "./microservices";
import { runCommand } from "./command";
type DecisionRecordType = "meeting" | "decision" | "goal" | "external_goal" | "internal_goal" | "blocker" | "debt" | "experiment";
type RequirementRecordType = Exclude<DecisionRecordType, "meeting">;
@@ -176,6 +177,106 @@ function decisionProxy(path: string, init?: { method?: string; body?: unknown })
return coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function decisionK8sPath(path: string): string {
const proxyPrefix = `/api/microservices/${encodeURIComponent(serviceId)}/proxy`;
if (path.startsWith(proxyPrefix)) return path.slice(proxyPrefix.length) || "/";
if (path === `/api/microservices/${encodeURIComponent(serviceId)}/health`) return "/health";
return path;
}
function decisionK8sTarget(): { node: string; namespace: string } {
const raw = readFileSync(resolve(repoRoot, "config", "unidesk-host-k8s.yaml"), "utf8");
const parsed = Bun.YAML.parse(raw) as unknown;
const record = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
const target = typeof record.target === "object" && record.target !== null && !Array.isArray(record.target) ? record.target as Record<string, unknown> : {};
const node = typeof target.id === "string" && target.id.length > 0 ? target.id : "NC01";
const namespace = typeof target.namespace === "string" && target.namespace.length > 0 ? target.namespace : "unidesk";
return { node, namespace };
}
async function nc01K8sDecisionFetch(path: string, init?: { method?: string; body?: unknown }): Promise<unknown> {
const target = decisionK8sTarget();
const payload = {
path: decisionK8sPath(path),
method: init?.method ?? "GET",
body: init?.body,
namespace: target.namespace,
};
const requestB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
const remoteScript = `
set -eu
POD=$(kubectl -n ${shellQuote(target.namespace)} get pod -l app.kubernetes.io/name=decision-center -o jsonpath='{.items[0].metadata.name}')
REQ_B64=${shellQuote(requestB64)}
kubectl -n ${shellQuote(target.namespace)} exec "$POD" -- env REQ_B64="$REQ_B64" bun -e '
const req = JSON.parse(Buffer.from(process.env.REQ_B64, "base64").toString("utf8"));
const headers = {};
const init = { method: req.method || "GET", headers };
if (req.body !== undefined) {
headers["content-type"] = "application/json";
init.body = JSON.stringify(req.body);
}
const res = await fetch("http://127.0.0.1:4277" + req.path, init);
const text = await res.text();
let body;
try { body = text ? JSON.parse(text) : null; } catch { body = text; }
if (!String(req.path).includes("includeBody=true") && body && typeof body === "object") {
for (const key of ["records", "entries", "requirements"]) {
if (Array.isArray(body[key])) {
body[key] = body[key].map((item) => item && typeof item === "object" ? { ...item, body: "" } : item);
}
}
}
let output = JSON.stringify({ ok: res.ok, status: res.status, body });
if (output.length > 8000 && body && typeof body === "object") {
for (const key of ["records", "entries", "requirements"]) {
if (Array.isArray(body[key])) {
const totalReturned = body[key].length;
body[key] = body[key].slice(0, 5).map((item) => {
if (!item || typeof item !== "object") return item;
return {
id: item.id,
docNo: item.docNo,
date: item.date,
title: item.title,
type: item.type,
status: item.status,
level: item.level,
sourceFile: item.sourceFile,
updatedAt: item.updatedAt,
};
});
body.totalReturned = totalReturned;
body[key + "Omitted"] = Math.max(0, totalReturned - body[key].length);
body.outputPolicy = { bounded: true, reason: "ssh-preview-budget", shown: body[key].length, totalReturned };
}
}
output = JSON.stringify({ ok: res.ok, status: res.status, body });
}
console.log(output);
'
`;
const result = runCommand(["bun", "scripts/cli.ts", "ssh", target.node, "sh", "--", remoteScript], repoRoot, { timeoutMs: 30_000 });
if (result.exitCode !== 0) {
return {
ok: false,
status: 502,
error: "NC01 decision-center k8s proxy failed",
commandExitCode: result.exitCode,
stdoutTail: result.stdout.slice(-1000),
stderrTail: result.stderr.slice(-1000),
};
}
const jsonLine = result.stdout.trim().split("\n").reverse().find((line) => line.trim().startsWith("{"));
if (jsonLine === undefined) {
return { ok: false, status: 502, error: "NC01 decision-center k8s proxy returned no JSON", stdoutTail: result.stdout.slice(-1000), stderrTail: result.stderr.slice(-1000) };
}
return JSON.parse(jsonLine) as unknown;
}
async function decisionProxyAsync(
fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>,
path: string,
@@ -555,35 +656,7 @@ async function updateRequirementAsync(id: string | undefined, args: string[], fe
}
export async function runDecisionCenterCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "list", id] = args;
if (action === "diary") {
const [diaryAction = "list", diaryId] = args.slice(1);
if (diaryAction === "import") return importDiary(args.slice(2));
if (diaryAction === "list") return listDiary(args.slice(2));
if (diaryAction === "history") return diaryHistory(args.slice(2));
if (diaryAction === "months") return listDiaryMonths();
if (diaryAction === "today") return args.includes("--edit") ? editTodayDiary(args.slice(2).filter((arg) => arg !== "--edit")) : todayDiary();
if (diaryAction === "show") return showDiary(diaryId, args.slice(3));
if (diaryAction === "edit" || diaryAction === "upsert") return editDiary(args.slice(2));
throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert");
}
if (action === "requirement" || action === "requirements") {
const [requirementAction = "list", requirementId] = args.slice(1);
if (requirementAction === "list") {
const query = recordQuery(args.slice(2), { requirementOnly: true });
return unwrapProxyResponse(decisionProxy(`/api/requirements${query ? `?${query}` : ""}`));
}
if (requirementAction === "create") return createRequirement(args.slice(2));
if (requirementAction === "upsert") return upsertRequirement(args.slice(2));
if (requirementAction === "show") return showRequirement(requirementId);
if (requirementAction === "update") return updateRequirement(requirementId, args.slice(3));
throw new Error("decision requirement command must be one of: list, create, show, update, upsert");
}
if (action === "upload") return uploadMeeting(args.slice(1));
if (action === "list") return listRecords(args.slice(1));
if (action === "show") return showRecord(id);
if (action === "health") return unwrapProxyResponse(coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/health`));
throw new Error("decision command must be one of: upload, list, show, health, requirement, diary");
return runDecisionCenterCommandAsync(_config, args, nc01K8sDecisionFetch);
}
export async function runDecisionCenterCommandAsync(