feat: add Sub2API runtime configuration CRUD
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
import type { UniDeskConfig } from "../config";
|
||||
|
||||
import { desiredAccountNames } from "./accounts";
|
||||
import { readCodexPoolConfig } from "./config";
|
||||
import { protectedManualAccountsForTarget } from "./public-exposure";
|
||||
import { renderSub2ApiTempUnschedulableCredentials } from "./redaction";
|
||||
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
|
||||
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
||||
|
||||
type RuntimeAction = "list" | "get" | "errors" | "apply" | "delete";
|
||||
|
||||
interface RuntimeOptions {
|
||||
action: RuntimeAction;
|
||||
account: string | null;
|
||||
template: string | null;
|
||||
kind: "temp-unschedulable";
|
||||
confirm: boolean;
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
since: string;
|
||||
tail: number;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
|
||||
const options = parseRuntimeOptions(args);
|
||||
const pool = readCodexPoolConfig();
|
||||
const target = codexPoolRuntimeTarget(options.targetId);
|
||||
const selectedTemplate = options.template === null ? null : pool.runtime.templatesById[options.template];
|
||||
if (options.template !== null && selectedTemplate === undefined) {
|
||||
throw new Error(`unknown runtime template ${options.template}; available: ${pool.runtime.templates.map((item) => item.id).join(", ")}`);
|
||||
}
|
||||
const payload = {
|
||||
action: options.action,
|
||||
account: options.account,
|
||||
kind: options.kind,
|
||||
confirm: options.confirm,
|
||||
full: options.full,
|
||||
since: options.since,
|
||||
tail: options.tail,
|
||||
groupName: pool.groupName,
|
||||
adminEmailDefault: pool.adminEmailDefault,
|
||||
managedAccountNames: desiredAccountNames(pool),
|
||||
protectedManualAccountNames: protectedManualAccountsForTarget(pool, target).map((item) => item.accountName),
|
||||
templates: pool.runtime.templates.map((template) => ({
|
||||
id: template.id,
|
||||
kind: template.kind,
|
||||
description: template.description,
|
||||
credentials: renderSub2ApiTempUnschedulableCredentials(template.tempUnschedulable),
|
||||
})),
|
||||
selectedTemplate: selectedTemplate === null || selectedTemplate === undefined ? null : {
|
||||
id: selectedTemplate.id,
|
||||
kind: selectedTemplate.kind,
|
||||
credentials: renderSub2ApiTempUnschedulableCredentials(selectedTemplate.tempUnschedulable),
|
||||
},
|
||||
};
|
||||
const result = await runRemoteCodexPoolScript(config, "runtime", runtimeScript(payload, target), target);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
return {
|
||||
ok,
|
||||
action: "platform-infra-sub2api-codex-pool-runtime",
|
||||
remote: compactCapture(result, { full: true }),
|
||||
parsed,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok,
|
||||
action: "platform-infra-sub2api-codex-pool-runtime",
|
||||
target: {
|
||||
id: target.id,
|
||||
route: target.route,
|
||||
namespace: target.namespace,
|
||||
runtimeMode: target.runtimeMode,
|
||||
endpoint: target.serviceDns,
|
||||
},
|
||||
request: {
|
||||
operation: options.action,
|
||||
account: options.account,
|
||||
template: options.template,
|
||||
kind: options.kind,
|
||||
confirm: options.confirm,
|
||||
since: options.action === "errors" ? options.since : undefined,
|
||||
tail: options.action === "errors" ? options.tail : undefined,
|
||||
},
|
||||
runtime: parsed,
|
||||
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 || parsed === null }),
|
||||
disclosure: options.action === "list"
|
||||
? {
|
||||
get: "bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id>",
|
||||
errors: "bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--account <name-or-id>] [--since 24h]",
|
||||
}
|
||||
: undefined,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
|
||||
const [actionRaw = "list", ...rest] = args;
|
||||
if (actionRaw !== "list" && actionRaw !== "get" && actionRaw !== "errors" && actionRaw !== "apply" && actionRaw !== "delete") {
|
||||
throw new Error("runtime usage: list|get|errors|apply|delete [--account <name-or-id>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable] [--confirm] [--target <id>] [--full|--raw]");
|
||||
}
|
||||
let account: string | null = null;
|
||||
let template: string | null = null;
|
||||
let kind = "temp-unschedulable" as const;
|
||||
let confirm = false;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let since = "24h";
|
||||
let tail = 50_000;
|
||||
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 === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
index += 1;
|
||||
return value.trim();
|
||||
};
|
||||
if (arg === "--confirm") confirm = true;
|
||||
else if (arg === "--full") full = true;
|
||||
else if (arg === "--raw") raw = true;
|
||||
else if (arg === "--account") account = readValue("--account");
|
||||
else if (arg.startsWith("--account=")) account = arg.slice("--account=".length).trim();
|
||||
else if (arg === "--template") template = readValue("--template");
|
||||
else if (arg.startsWith("--template=")) template = arg.slice("--template=".length).trim();
|
||||
else if (arg === "--kind") kind = parseRuntimeKind(readValue("--kind"));
|
||||
else if (arg.startsWith("--kind=")) kind = parseRuntimeKind(arg.slice("--kind=".length));
|
||||
else if (arg === "--target") targetId = readValue("--target");
|
||||
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
|
||||
else if (arg === "--since") since = readValue("--since");
|
||||
else if (arg.startsWith("--since=")) since = arg.slice("--since=".length).trim();
|
||||
else if (arg === "--tail") tail = parseRuntimeTail(readValue("--tail"));
|
||||
else if (arg.startsWith("--tail=")) tail = parseRuntimeTail(arg.slice("--tail=".length));
|
||||
else throw new Error(`unsupported runtime option: ${arg}`);
|
||||
}
|
||||
if ((actionRaw === "get" || actionRaw === "apply" || actionRaw === "delete") && !account) {
|
||||
throw new Error(`runtime ${actionRaw} requires --account <name-or-id>`);
|
||||
}
|
||||
if (account !== null && (account.length > 256 || /[\r\n]/u.test(account))) throw new Error("--account has an unsupported format");
|
||||
if (actionRaw === "apply" && !template) throw new Error("runtime apply requires --template <id>");
|
||||
if (actionRaw !== "apply" && template !== null) throw new Error(`runtime ${actionRaw} does not accept --template`);
|
||||
if (actionRaw !== "errors" && (since !== "24h" || tail !== 50_000)) throw new Error(`runtime ${actionRaw} does not accept --since or --tail`);
|
||||
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
|
||||
if ((actionRaw === "list" || actionRaw === "get" || actionRaw === "errors") && confirm) throw new Error(`runtime ${actionRaw} does not accept --confirm`);
|
||||
if (full && raw) throw new Error("use only one of --full or --raw");
|
||||
return { action: actionRaw, account, template, kind, confirm, full, raw, since, tail, targetId };
|
||||
}
|
||||
|
||||
function parseRuntimeKind(value: string): "temp-unschedulable" {
|
||||
if (value !== "temp-unschedulable") throw new Error("--kind must be temp-unschedulable");
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseRuntimeTail(value: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed < 100 || parsed > 200_000) throw new Error("--tail must be an integer from 100 to 200000");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function pyJson(value: unknown): string {
|
||||
return `json.loads(${JSON.stringify(JSON.stringify(value))})`;
|
||||
}
|
||||
|
||||
function runtimeScript(payload: unknown, target: ReturnType<typeof codexPoolRuntimeTarget>): string {
|
||||
return `
|
||||
set -u
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
RUNTIME_MODE = ${pyJson(target.runtimeMode)}
|
||||
NAMESPACE = ${pyJson(target.namespace)}
|
||||
HOST_DOCKER_APP_PORT = ${pyJson(target.hostDockerAppPort)}
|
||||
HOST_DOCKER_ENV_PATH = ${pyJson(target.hostDockerEnvPath)}
|
||||
HOST_DOCKER_APP_CONTAINER = "sub2api-app"
|
||||
APP_SECRET_NAME = ${pyJson(target.appSecretName)}
|
||||
PAYLOAD = ${pyJson(payload)}
|
||||
|
||||
def run(cmd, input_bytes=None):
|
||||
return subprocess.run(cmd, input=input_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
def text(value, limit=1000):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8", errors="replace")
|
||||
return str(value)[-limit:]
|
||||
|
||||
def docker(args):
|
||||
proc = run(["docker", *args])
|
||||
if proc.returncode == 0:
|
||||
return proc
|
||||
sudo_proc = run(["sudo", "-n", "docker", *args])
|
||||
return sudo_proc if sudo_proc.returncode == 0 else proc
|
||||
|
||||
def kubectl(args, input_bytes=None):
|
||||
return run(["kubectl", *args], input_bytes)
|
||||
|
||||
def kube_json(args, label):
|
||||
proc = kubectl([*args, "-o", "json"])
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"{label} failed: {text(proc.stderr)}")
|
||||
return json.loads(proc.stdout.decode("utf-8"))
|
||||
|
||||
def read_host_env():
|
||||
try:
|
||||
with open(HOST_DOCKER_ENV_PATH, "r", encoding="utf-8") as handle:
|
||||
lines = handle.read().splitlines()
|
||||
except PermissionError:
|
||||
proc = run(["sudo", "-n", "cat", HOST_DOCKER_ENV_PATH])
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("read host env failed: " + text(proc.stderr))
|
||||
lines = proc.stdout.decode("utf-8", errors="replace").splitlines()
|
||||
values = {}
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||
continue
|
||||
key, value = stripped.split("=", 1)
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||
value = value[1:-1]
|
||||
values[key.strip()] = value
|
||||
return values
|
||||
|
||||
def select_app_pod():
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
return HOST_DOCKER_APP_CONTAINER
|
||||
pods = kube_json(["-n", NAMESPACE, "get", "pods", "-l", "app.kubernetes.io/name=sub2api"], "sub2api pods").get("items") or []
|
||||
running = [item for item in pods if (item.get("status") or {}).get("phase") == "Running"]
|
||||
if not running:
|
||||
raise RuntimeError("sub2api app pod not found")
|
||||
return running[0]["metadata"]["name"]
|
||||
|
||||
APP_POD = select_app_pod()
|
||||
|
||||
def config_value(name, key, fallback=None):
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
return read_host_env().get(key) or fallback
|
||||
data = kube_json(["-n", NAMESPACE, "get", "configmap", name], f"configmap/{name}").get("data") or {}
|
||||
return data.get(key) or fallback
|
||||
|
||||
def secret_value(name, key):
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
return read_host_env().get(key)
|
||||
import base64
|
||||
data = kube_json(["-n", NAMESPACE, "get", "secret", name], f"secret/{name}").get("data") or {}
|
||||
return base64.b64decode(data[key]).decode("utf-8") if key in data else None
|
||||
|
||||
def parse_curl(proc):
|
||||
output = proc.stdout.decode("utf-8", errors="replace")
|
||||
marker = "\\n__HTTP_CODE__:"
|
||||
pos = output.rfind(marker)
|
||||
if pos < 0:
|
||||
return {"ok": False, "httpStatus": 0, "json": None, "body": output, "stderr": text(proc.stderr)}
|
||||
body = output[:pos]
|
||||
try:
|
||||
status = int(output[pos + len(marker):].strip()[-3:])
|
||||
except ValueError:
|
||||
status = 0
|
||||
try:
|
||||
parsed = json.loads(body) if body.strip() else None
|
||||
except json.JSONDecodeError:
|
||||
parsed = None
|
||||
return {"ok": proc.returncode == 0 and 200 <= status < 300, "httpStatus": status, "json": parsed, "body": body, "stderr": text(proc.stderr)}
|
||||
|
||||
def curl_api(method, path, bearer=None, payload=None):
|
||||
body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
script = r'''
|
||||
set -eu
|
||||
method="$1"
|
||||
url="$2"
|
||||
token="\${3:-}"
|
||||
tmp="$(mktemp)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
cat > "$tmp"
|
||||
if [ -n "$token" ] && [ "$method" = GET ] && [ ! -s "$tmp" ]; then
|
||||
curl -sS -w '\n__HTTP_CODE__:%{http_code}' -X "$method" -H "Authorization: Bearer $token" "$url"
|
||||
elif [ -n "$token" ]; then
|
||||
curl -sS -w '\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" --data-binary @"$tmp" "$url"
|
||||
elif [ "$method" = GET ] && [ ! -s "$tmp" ]; then
|
||||
curl -sS -w '\n__HTTP_CODE__:%{http_code}' -X "$method" "$url"
|
||||
else
|
||||
curl -sS -w '\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' --data-binary @"$tmp" "$url"
|
||||
fi
|
||||
'''
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
proc = run(["sh", "-c", script, "sh", method, f"http://127.0.0.1:{HOST_DOCKER_APP_PORT}{path}", bearer or ""], body)
|
||||
else:
|
||||
proc = run(["kubectl", "-n", NAMESPACE, "exec", "-i", APP_POD, "--", "sh", "-c", script, "sh", method, f"http://127.0.0.1:8080{path}", bearer or ""], body)
|
||||
return parse_curl(proc)
|
||||
|
||||
def data_of(response, label):
|
||||
parsed = response.get("json")
|
||||
code = parsed.get("code") if isinstance(parsed, dict) else None
|
||||
if response.get("ok") is not True or (code is not None and code != 0):
|
||||
message = parsed.get("message") if isinstance(parsed, dict) else text(response.get("body"), 500)
|
||||
raise RuntimeError(f"{label} failed: http={response.get('httpStatus')} message={message}")
|
||||
return parsed.get("data") if isinstance(parsed, dict) and "data" in parsed else parsed
|
||||
|
||||
def items_of(data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
for key in ("items", "groups", "accounts"):
|
||||
if isinstance(data.get(key), list):
|
||||
return data[key]
|
||||
return []
|
||||
|
||||
def login():
|
||||
email = config_value("sub2api-config", "ADMIN_EMAIL", PAYLOAD["adminEmailDefault"])
|
||||
password = secret_value(APP_SECRET_NAME, "ADMIN_PASSWORD")
|
||||
if not password:
|
||||
raise RuntimeError("ADMIN_PASSWORD missing")
|
||||
data = data_of(curl_api("POST", "/api/v1/auth/login", payload={"email": email, "password": password}), "admin login")
|
||||
token = None
|
||||
if isinstance(data, dict):
|
||||
token = data.get("access_token") or data.get("token")
|
||||
if not token:
|
||||
raise RuntimeError("admin login response has no token")
|
||||
return token
|
||||
|
||||
def list_groups(token):
|
||||
return items_of(data_of(curl_api("GET", "/api/v1/admin/groups/all?platform=openai", bearer=token), "list groups"))
|
||||
|
||||
def list_group_accounts(token, group_id):
|
||||
path = f"/api/v1/admin/accounts?group_id={group_id}&page=1&page_size=500&platform=openai"
|
||||
return items_of(data_of(curl_api("GET", path, bearer=token), "list group accounts"))
|
||||
|
||||
def account_detail(token, account_id):
|
||||
return data_of(curl_api("GET", f"/api/v1/admin/accounts/{account_id}", bearer=token), "get account")
|
||||
|
||||
def normalize_policy(credentials):
|
||||
if not isinstance(credentials, dict):
|
||||
credentials = {}
|
||||
enabled = credentials.get("temp_unschedulable_enabled") is True
|
||||
raw = credentials.get("temp_unschedulable_rules")
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
raw = []
|
||||
rules = raw if isinstance(raw, list) else []
|
||||
codes = sorted(set(item.get("error_code") for item in rules if isinstance(item, dict) and isinstance(item.get("error_code"), int)))
|
||||
return {"enabled": enabled, "ruleCount": len(rules), "statusCodes": codes, "rules": rules}
|
||||
|
||||
def template_summary(template):
|
||||
policy = normalize_policy(template.get("credentials"))
|
||||
return {"id": template.get("id"), "kind": template.get("kind"), "description": template.get("description"), **{key: policy[key] for key in ("enabled", "ruleCount", "statusCodes")}}
|
||||
|
||||
def matching_template(credentials):
|
||||
current = normalize_policy(credentials)
|
||||
for template in PAYLOAD["templates"]:
|
||||
if normalize_policy(template.get("credentials")) == current:
|
||||
return template.get("id")
|
||||
return None
|
||||
|
||||
def account_summary(detail, include_rules=False):
|
||||
credentials = detail.get("credentials") if isinstance(detail.get("credentials"), dict) else {}
|
||||
policy = normalize_policy(credentials)
|
||||
name = detail.get("name")
|
||||
management = "yaml-managed" if name in PAYLOAD["managedAccountNames"] else ("yaml-protected-manual" if name in PAYLOAD["protectedManualAccountNames"] else "runtime-manual")
|
||||
result = {
|
||||
"accountName": name,
|
||||
"accountId": detail.get("id"),
|
||||
"management": management,
|
||||
"type": detail.get("type"),
|
||||
"status": detail.get("status"),
|
||||
"schedulable": detail.get("schedulable"),
|
||||
"proxyId": detail.get("proxy_id"),
|
||||
"tempUnschedulable": {key: policy[key] for key in ("enabled", "ruleCount", "statusCodes")},
|
||||
"matchingTemplate": matching_template(credentials),
|
||||
"poolModeEnabled": credentials.get("pool_mode") is True,
|
||||
"credentialsPrinted": False,
|
||||
}
|
||||
if include_rules:
|
||||
result["tempUnschedulable"]["rules"] = policy["rules"]
|
||||
return result
|
||||
|
||||
def policy_summary(policy, include_rules=False):
|
||||
keys = ("enabled", "ruleCount", "statusCodes", "rules") if include_rules else ("enabled", "ruleCount", "statusCodes")
|
||||
return {key: policy[key] for key in keys}
|
||||
|
||||
def find_account(token, accounts, selector):
|
||||
exact = [item for item in accounts if str(item.get("id")) == selector or item.get("name") == selector]
|
||||
if len(exact) != 1:
|
||||
raise RuntimeError("account-not-found-or-ambiguous: " + selector)
|
||||
return account_detail(token, exact[0]["id"])
|
||||
|
||||
def runtime_log_lines():
|
||||
since = PAYLOAD.get("since", "24h")
|
||||
tail = str(PAYLOAD.get("tail", 50000))
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
proc = docker(["logs", "--since", since, "--tail", tail, APP_POD])
|
||||
else:
|
||||
proc = kubectl(["-n", NAMESPACE, "logs", APP_POD, "--since=" + since, "--tail=" + tail])
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("read Sub2API runtime logs failed: " + text(proc.stderr))
|
||||
output = proc.stdout + b"\\n" + proc.stderr
|
||||
return output.decode("utf-8", errors="replace").splitlines()
|
||||
|
||||
def compact_error(value):
|
||||
if not isinstance(value, str):
|
||||
return ""
|
||||
return " ".join(value.replace("\x00", "").split())[:240]
|
||||
|
||||
def observed_runtime_errors(accounts):
|
||||
selector = PAYLOAD.get("account")
|
||||
selected = [item for item in accounts if selector is None or str(item.get("id")) == str(selector) or item.get("name") == selector]
|
||||
if selector is not None and len(selected) != 1:
|
||||
raise RuntimeError("account-not-found-or-ambiguous: " + str(selector))
|
||||
selected_ids = set(item.get("id") for item in selected)
|
||||
stats = {}
|
||||
for item in selected:
|
||||
stats[item.get("id")] = {
|
||||
"accountName": item.get("name"),
|
||||
"accountId": item.get("id"),
|
||||
"upstreamEventCount": 0,
|
||||
"observedStatusCodes": {},
|
||||
"forwardFailureCount": 0,
|
||||
"forwardFailureStatusCodes": {},
|
||||
"lastSeenAt": None,
|
||||
"errorSamples": [],
|
||||
}
|
||||
scanned = 0
|
||||
for line in runtime_log_lines():
|
||||
if "account_upstream_error" not in line and "openai.forward_failed" not in line:
|
||||
continue
|
||||
json_start = line.find("{")
|
||||
if json_start < 0:
|
||||
continue
|
||||
try:
|
||||
item = json.loads(line[json_start:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
account_id = item.get("account_id") if isinstance(item, dict) else None
|
||||
if account_id not in selected_ids:
|
||||
continue
|
||||
scanned += 1
|
||||
at = line.split("\t", 1)[0].strip() or None
|
||||
current = stats[account_id]
|
||||
if at and (current["lastSeenAt"] is None or at > current["lastSeenAt"]):
|
||||
current["lastSeenAt"] = at
|
||||
if "account_upstream_error" in line:
|
||||
code = item.get("status_code")
|
||||
if isinstance(code, int):
|
||||
key = str(code)
|
||||
current["observedStatusCodes"][key] = current["observedStatusCodes"].get(key, 0) + 1
|
||||
current["upstreamEventCount"] += 1
|
||||
continue
|
||||
current["forwardFailureCount"] += 1
|
||||
error = compact_error(item.get("error"))
|
||||
match = re.search(r"(?:upstream error:\\s*|status(?:_code)?[=:]\\s*)(\\d{3})", error, re.IGNORECASE)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
current["forwardFailureStatusCodes"][key] = current["forwardFailureStatusCodes"].get(key, 0) + 1
|
||||
if error and error not in current["errorSamples"] and len(current["errorSamples"]) < 3:
|
||||
current["errorSamples"].append(error)
|
||||
rows = []
|
||||
aggregate = {}
|
||||
for item in selected:
|
||||
current = stats[item.get("id")]
|
||||
observed = [{"statusCode": int(code), "count": count} for code, count in sorted(current["observedStatusCodes"].items(), key=lambda pair: int(pair[0]))]
|
||||
forwarded = [{"statusCode": int(code), "count": count} for code, count in sorted(current["forwardFailureStatusCodes"].items(), key=lambda pair: int(pair[0]))]
|
||||
for entry in observed:
|
||||
key = str(entry["statusCode"])
|
||||
aggregate[key] = aggregate.get(key, 0) + entry["count"]
|
||||
current["observedStatusCodes"] = observed
|
||||
current["forwardFailureStatusCodes"] = forwarded
|
||||
if PAYLOAD.get("full") is not True:
|
||||
current["errorSamples"] = current["errorSamples"][:1]
|
||||
if current["upstreamEventCount"] > 0 or current["forwardFailureCount"] > 0 or selector is not None:
|
||||
rows.append(current)
|
||||
return {
|
||||
"window": {"since": PAYLOAD.get("since"), "tail": PAYLOAD.get("tail"), "matchedLogEvents": scanned},
|
||||
"aggregateObservedStatusCodes": [{"statusCode": int(code), "count": count} for code, count in sorted(aggregate.items(), key=lambda pair: int(pair[0]))],
|
||||
"accountCountWithErrors": sum(1 for item in rows if item["upstreamEventCount"] > 0 or item["forwardFailureCount"] > 0),
|
||||
"accounts": rows,
|
||||
"message": "observedStatusCodes comes only from account_upstream_error; forward failures are supplemental message evidence",
|
||||
}
|
||||
|
||||
def runtime_result():
|
||||
token = login()
|
||||
group = next((item for item in list_groups(token) if item.get("name") == PAYLOAD["groupName"]), None)
|
||||
if not isinstance(group, dict) or group.get("id") is None:
|
||||
raise RuntimeError("pool-group-not-found")
|
||||
accounts = list_group_accounts(token, group["id"])
|
||||
base = {
|
||||
"group": {"id": group.get("id"), "name": group.get("name"), "status": group.get("status")},
|
||||
"templates": [template_summary(item) for item in PAYLOAD["templates"]],
|
||||
"endpoint": "host-docker" if RUNTIME_MODE == "host-docker" else "k3s-service",
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
action = PAYLOAD["action"]
|
||||
if action == "list":
|
||||
details = [account_detail(token, item["id"]) for item in accounts]
|
||||
return {**base, "ok": True, "operation": "list", "mutation": False, "accountCount": len(details), "accounts": [account_summary(item) for item in sorted(details, key=lambda value: value.get("name") or "")]}
|
||||
if action == "errors":
|
||||
errors = observed_runtime_errors(accounts)
|
||||
return {**base, "ok": True, "operation": "errors", "mutation": False, "errors": errors}
|
||||
detail = find_account(token, accounts, str(PAYLOAD["account"]))
|
||||
if action == "get":
|
||||
return {**base, "ok": True, "operation": "get", "mutation": False, "account": account_summary(detail, True)}
|
||||
if detail.get("type") != "apikey":
|
||||
raise RuntimeError("runtime temp-unschedulable policy requires an apikey account")
|
||||
credentials = dict(detail.get("credentials") or {})
|
||||
before = normalize_policy(credentials)
|
||||
if action == "apply":
|
||||
template = PAYLOAD.get("selectedTemplate")
|
||||
if not isinstance(template, dict):
|
||||
raise RuntimeError("selected runtime template missing")
|
||||
desired_fields = template.get("credentials") or {}
|
||||
desired = dict(credentials)
|
||||
desired.update(desired_fields)
|
||||
after_plan = normalize_policy(desired)
|
||||
exists = "temp_unschedulable_enabled" in credentials or "temp_unschedulable_rules" in credentials
|
||||
change = "noop" if before == after_plan else ("update" if exists else "create")
|
||||
else:
|
||||
desired = dict(credentials)
|
||||
desired.pop("temp_unschedulable_enabled", None)
|
||||
desired.pop("temp_unschedulable_rules", None)
|
||||
after_plan = normalize_policy(desired)
|
||||
change = "delete" if before != after_plan or "temp_unschedulable_enabled" in credentials or "temp_unschedulable_rules" in credentials else "noop"
|
||||
include_plan_rules = PAYLOAD.get("full") is True
|
||||
plan = {
|
||||
"change": change,
|
||||
"account": account_summary(detail, include_plan_rules),
|
||||
"before": policy_summary(before, include_plan_rules),
|
||||
"after": policy_summary(after_plan, include_plan_rules),
|
||||
"template": PAYLOAD.get("selectedTemplate", {}).get("id") if isinstance(PAYLOAD.get("selectedTemplate"), dict) else None,
|
||||
"credentialsPrinted": False,
|
||||
}
|
||||
if PAYLOAD.get("confirm") is not True or change == "noop":
|
||||
return {**base, "ok": True, "operation": action, "mode": "dry-run" if PAYLOAD.get("confirm") is not True else "confirmed-noop", "mutation": False, "plan": plan}
|
||||
updated = data_of(curl_api("PUT", f"/api/v1/admin/accounts/{detail['id']}", bearer=token, payload={"credentials": desired}), "update account runtime config")
|
||||
after_detail = updated if isinstance(updated, dict) else account_detail(token, detail["id"])
|
||||
return {**base, "ok": True, "operation": action, "mode": "confirmed", "mutation": True, "plan": plan, "account": account_summary(after_detail, include_plan_rules)}
|
||||
|
||||
try:
|
||||
result = runtime_result()
|
||||
except Exception as exc:
|
||||
result = {"ok": False, "operation": PAYLOAD.get("action"), "mutation": False, "error": str(exc), "valuesPrinted": False}
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if result.get("ok") else 1)
|
||||
PY
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user