Files
pikasTech-unidesk/scripts/src/platform-infra-sub2api-codex/error-passthrough.ts
T
2026-07-14 20:40:15 +02:00

267 lines
14 KiB
TypeScript

import type { UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { readCodexPoolConfig } from "./config";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { codexPoolConfigPath, type CodexErrorPassthroughRule } from "./types";
type Action = "list" | "apply" | "delete";
interface Options {
action: Action;
template: string | null;
confirm: boolean;
json: boolean;
targetId: string;
}
export async function codexPoolErrorPassthrough(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
if (args.includes("--help")) return help();
const options = parseOptions(args);
const pool = readCodexPoolConfig();
const target = codexPoolRuntimeTarget(options.targetId);
const selected = options.template === null ? null : pool.runtime.errorPassthroughRulesById[options.template];
if (options.template !== null && selected === undefined) {
throw new Error(`unknown error-passthrough template ${options.template}; available: ${pool.runtime.errorPassthroughRules.map((item) => item.id).join(", ")}`);
}
const payload = {
action: options.action,
confirm: options.confirm,
selected: selected === null || selected === undefined ? null : apiRule(selected),
managed: pool.runtime.errorPassthroughRules.map((rule) => ({ id: rule.id, ...apiRule(rule) })),
};
const result = await runRemoteCodexPoolScript(config, "error-passthrough", remoteScript(payload, target), target);
const parsed = parseJsonOutput(result.stdout);
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
const response = {
ok,
action: `platform-infra-sub2api-codex-pool-error-passthrough-${options.action}`,
target: { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns },
source: { configPath: `${codexPoolConfigPath}#runtime.errorPassthroughRules`, api: "/api/v1/admin/error-passthrough-rules", official: true },
request: { template: options.template, confirm: options.confirm },
result: parsed,
remote: compactCapture(result, { full: parsed === null || result.exitCode !== 0 }),
valuesPrinted: false,
};
return options.json ? response : render(response);
}
function parseOptions(args: string[]): Options {
const [actionRaw = "list", ...rest] = args;
if (actionRaw !== "list" && actionRaw !== "apply" && actionRaw !== "delete") throw new Error("error-passthrough usage: list|apply|delete [--template <id>] [--confirm] [--target <id>] [--json]");
let template: string | null = null;
let confirm = false;
let json = false;
let targetId = defaultCodexPoolRuntimeTargetId();
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index]!;
const readValue = (name: string): string => {
const value = rest[index + 1];
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
index += 1;
return value;
};
if (arg === "--template") template = readValue("--template");
else if (arg.startsWith("--template=")) template = arg.slice("--template=".length);
else if (arg === "--target") targetId = readValue("--target");
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length);
else if (arg === "--confirm") confirm = true;
else if (arg === "--json") json = true;
else throw new Error(`unsupported error-passthrough option: ${arg}`);
}
if ((actionRaw === "apply" || actionRaw === "delete") && template === null) throw new Error(`error-passthrough ${actionRaw} requires --template <id>`);
if (actionRaw === "list" && (template !== null || confirm)) throw new Error("error-passthrough list does not accept --template or --confirm");
return { action: actionRaw, template, confirm, json, targetId };
}
function apiRule(rule: CodexErrorPassthroughRule): Record<string, unknown> {
return {
name: rule.name,
enabled: rule.enabled,
priority: rule.priority,
error_codes: rule.errorCodes,
keywords: rule.keywords,
match_mode: rule.matchMode,
platforms: rule.platforms,
passthrough_code: rule.passthroughCode,
response_code: rule.responseCode,
passthrough_body: rule.passthroughBody,
custom_message: rule.customMessage,
skip_monitoring: rule.skipMonitoring,
description: rule.description,
};
}
function help(): RenderedCliResult {
return {
ok: true,
command: "platform-infra sub2api codex-pool error-passthrough --help",
renderedText: [
"SUB2API ERROR PASSTHROUGH",
"Usage:",
" bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list [--target PK01] [--json]",
" bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough apply --template <id> [--target PK01] [--confirm] [--json]",
" bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough delete --template <id> [--target PK01] [--confirm] [--json]",
"Writes use the official Sub2API admin error-passthrough-rules API. Without --confirm, apply/delete are dry-run.",
].join("\n"),
contentType: "text/plain",
};
}
function render(response: Record<string, unknown>): RenderedCliResult {
const result = record(response.result);
const rows = Array.isArray(result.rules) ? result.rules.map(record) : [];
const lines = [
`SUB2API ERROR PASSTHROUGH ok=${response.ok === true ? "true" : "false"} mode=${text(result.mode)} mutation=${text(result.mutation)}`,
"TEMPLATE RULE_ID ENABLED MATCH ERROR_CODES RESPONSE PLATFORMS STATE",
];
for (const row of rows) {
lines.push([
text(row.templateId).padEnd(36),
text(row.ruleId).padEnd(7),
text(row.enabled).padEnd(7),
text(row.matchMode).padEnd(5),
text(row.errorCodes).padEnd(11),
text(row.responseCode).padEnd(8),
text(row.platforms).padEnd(9),
text(row.state),
].join(" ").trimEnd());
}
if (result.unmanagedCount !== undefined) lines.push(`UNMANAGED ${text(result.unmanagedCount)}`);
if (result.message) lines.push(`MESSAGE ${text(result.message)}`);
return { ok: response.ok === true, command: String(response.action), renderedText: lines.join("\n"), contentType: "text/plain", projection: response };
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function text(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
if (Array.isArray(value)) return value.join(",");
if (typeof value === "boolean") return value ? "yes" : "no";
return String(value).replace(/[\r\n\t]+/gu, " ");
}
function remoteScript(payload: unknown, target: ReturnType<typeof codexPoolRuntimeTarget>): string {
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return `set -u
python3 - <<'PY'
import base64, json, subprocess
from urllib.parse import urlencode
PAYLOAD = json.loads(base64.b64decode(${JSON.stringify(encoded)}).decode("utf-8"))
RUNTIME_MODE = ${JSON.stringify(target.runtimeMode)}
NAMESPACE = ${JSON.stringify(target.namespace)}
APP_SECRET_NAME = ${JSON.stringify(target.appSecretName)}
HOST_PORT = ${JSON.stringify(target.hostDockerAppPort)}
APP_POD = None
def run(cmd, body=None):
return subprocess.run(cmd, input=body, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def docker(args):
proc = run(["docker", *args])
return proc if proc.returncode == 0 else run(["sudo", "-n", "docker", *args])
def kubectl(args, body=None):
return run(["kubectl", *args], body)
def kube_json(args):
proc = kubectl([*args, "-o", "json"])
if proc.returncode != 0: raise RuntimeError("kubectl read failed")
return json.loads(proc.stdout.decode())
if RUNTIME_MODE != "host-docker":
pods = kube_json(["-n", NAMESPACE, "get", "pods", "-l", "app=sub2api"]).get("items") or []
running = [x for x in pods if x.get("status", {}).get("phase") == "Running"]
if not running: raise RuntimeError("sub2api app pod not found")
APP_POD = running[0]["metadata"]["name"]
def env_value(key, default=None):
if RUNTIME_MODE == "host-docker":
proc = docker(["inspect", "sub2api-app", "--format", "{{json .Config.Env}}"])
if proc.returncode != 0: return default
for item in json.loads(proc.stdout.decode()):
if item.startswith(key + "="): return item.split("=", 1)[1]
return default
cm = kube_json(["-n", NAMESPACE, "get", "configmap", "sub2api-config"]).get("data") or {}
return cm.get(key, default)
def secret_value(key):
if RUNTIME_MODE == "host-docker": return env_value(key)
data = kube_json(["-n", NAMESPACE, "get", "secret", APP_SECRET_NAME]).get("data") or {}
return base64.b64decode(data[key]).decode() if key in data else None
def api(method, path, token=None, payload=None):
body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode()
script = '''set -eu
method="$1"; url="$2"; token="$3"; tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT; cat >"$tmp"
args=""; [ -n "$token" ] && args="Authorization: Bearer $token"
if [ -n "$args" ] && [ -s "$tmp" ]; then curl -sS -w '\n__CODE__:%{http_code}' -X "$method" -H "$args" -H 'Content-Type: application/json' --data-binary @"$tmp" "$url"
elif [ -n "$args" ]; then curl -sS -w '\n__CODE__:%{http_code}' -X "$method" -H "$args" "$url"
elif [ -s "$tmp" ]; then curl -sS -w '\n__CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' --data-binary @"$tmp" "$url"
else curl -sS -w '\n__CODE__:%{http_code}' -X "$method" "$url"; fi'''
base = f"http://127.0.0.1:{HOST_PORT}" if RUNTIME_MODE == "host-docker" else "http://127.0.0.1:8080"
cmd = ["sh", "-c", script, "sh", method, base + path, token or ""]
proc = run(cmd, body) if RUNTIME_MODE == "host-docker" else kubectl(["-n", NAMESPACE, "exec", "-i", APP_POD, "--", *cmd], body)
out = proc.stdout.decode(errors="replace"); marker = "\\n__CODE__:"; pos = out.rfind(marker)
code = int(out[pos + len(marker):].strip()) if pos >= 0 else 0; raw = out[:pos] if pos >= 0 else out
parsed = json.loads(raw) if raw.strip() else None
if proc.returncode != 0 or not 200 <= code < 300 or (isinstance(parsed, dict) and parsed.get("code") not in (None, 0)):
raise RuntimeError(f"official admin API failed method={method} path={path} http={code}")
return parsed.get("data") if isinstance(parsed, dict) and "data" in parsed else parsed
def login():
password = secret_value("ADMIN_PASSWORD")
if not password: raise RuntimeError("ADMIN_PASSWORD missing")
data = api("POST", "/api/v1/auth/login", payload={"email": env_value("ADMIN_EMAIL", "admin@example.com"), "password": password})
token = data.get("access_token") or data.get("token") if isinstance(data, dict) else None
if not token: raise RuntimeError("admin login response has no token")
return token
FIELDS = ("name","enabled","priority","error_codes","keywords","match_mode","platforms","passthrough_code","response_code","passthrough_body","custom_message","skip_monitoring","description")
def normalized(rule): return {k: rule.get(k) for k in FIELDS}
def state(current, desired):
if current is None: return "missing"
return "synced" if normalized(current) == normalized(desired) else "drift"
def execute():
token = login(); current = api("GET", "/api/v1/admin/error-passthrough-rules", token=token) or []
if not isinstance(current, list): raise RuntimeError("official rule list is not an array")
managed = PAYLOAD.get("managed") or []; selected = PAYLOAD.get("selected")
action = PAYLOAD["action"]; confirm = bool(PAYLOAD.get("confirm")); mutation = False
if action in ("apply", "delete"):
matches = [r for r in current if r.get("name") == selected.get("name")]
if len(matches) > 1: raise RuntimeError("duplicate official rules with managed name")
existing = matches[0] if matches else None
if action == "apply" and state(existing, selected) != "synced" and confirm:
if existing is None: api("POST", "/api/v1/admin/error-passthrough-rules", token, selected)
else: api("PUT", f"/api/v1/admin/error-passthrough-rules/{existing['id']}", token, selected)
mutation = True
if action == "delete" and existing is not None and confirm:
api("DELETE", f"/api/v1/admin/error-passthrough-rules/{existing['id']}", token)
mutation = True
current = api("GET", "/api/v1/admin/error-passthrough-rules", token=token) or []
rows = []
names = {m["name"] for m in managed}
for desired in managed:
found = [r for r in current if r.get("name") == desired["name"]]
actual = found[0] if len(found) == 1 else None
row_state = "duplicate" if len(found) > 1 else state(actual, desired)
if action == "delete" and selected and desired["name"] == selected["name"] and actual is None: row_state = "absent"
rows.append({"templateId": desired["id"], "ruleId": actual.get("id") if actual else None, "enabled": actual.get("enabled") if actual else desired.get("enabled"), "matchMode": desired.get("match_mode"), "errorCodes": desired.get("error_codes"), "responseCode": desired.get("response_code"), "platforms": desired.get("platforms"), "state": row_state})
target_row = next((x for x in rows if selected and x["templateId"] == next((m["id"] for m in managed if m["name"] == selected["name"]), None)), None)
ok = all(x["state"] not in ("duplicate",) for x in rows)
if action == "apply" and confirm: ok = ok and target_row is not None and target_row["state"] == "synced"
if action == "delete" and confirm: ok = ok and target_row is not None and target_row["state"] == "absent"
return {"ok": ok, "mode": "confirm" if confirm else "dry-run" if action != "list" else "list", "mutation": mutation, "rules": rows, "unmanagedCount": sum(1 for r in current if r.get("name") not in names), "message": "official Sub2API error-passthrough-rules API"}
try: output = execute()
except Exception as exc: output = {"ok": False, "error": str(exc), "mutation": False, "rules": []}
print(json.dumps(output, ensure_ascii=False, indent=2))
raise SystemExit(0 if output.get("ok") else 1)
PY`;
}