cicd summarize follower state on target node

This commit is contained in:
Codex
2026-07-03 12:05:26 +00:00
parent 67008b9f89
commit 5d6edbdfe4
4 changed files with 194 additions and 78 deletions
+23 -78
View File
@@ -3,7 +3,6 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { isAbsolute } from "node:path";
import { gunzipSync } from "node:zlib";
import { repoRoot, rootPath, type UniDeskConfig } from "./config";
import { runCommand, type CommandResult } from "./command";
import { startJob } from "./jobs";
@@ -1546,7 +1545,11 @@ const NATIVE_BUNDLE_SCRIPT_NAMES = [
] as const;
function nativeBundleScriptLoadShell(): string {
return NATIVE_BUNDLE_SCRIPT_NAMES.map((name) => {
return nativeScriptLoadShell(NATIVE_BUNDLE_SCRIPT_NAMES);
}
function nativeScriptLoadShell(names: readonly string[]): string {
return names.map((name) => {
const encoded = Buffer.from(readFileSync(rootPath("scripts/native/cicd", name), "utf8"), "utf8").toString("base64");
return [
`printf '%s' ${shQuote(encoded)} | base64 -d > "$tmpdir/${name}"`,
@@ -1850,95 +1853,37 @@ function readK8sState(registry: BranchFollowerRegistry, options: ParsedOptions):
}
function kubeConfigMapFollowerState(registry: BranchFollowerRegistry, options: ParsedOptions): K8sFollowerStateRead {
const stateByFollower: Record<string, Record<string, unknown>> = {};
const errors: string[] = [];
let present = false;
for (const follower of registry.followers) {
const result = kubeConfigMapDataValue(registry, options, follower.id);
if (!result.ok) {
errors.push(result.error);
continue;
}
if (!result.present) {
return { ok: true, stateByFollower: {}, present: false, error: "" };
}
present = true;
if (result.omitted) continue;
if (result.value === null || result.value.length === 0) continue;
const parsed = parseJsonObject(result.value);
if (parsed === null) {
errors.push(`${follower.id}: invalid state json`);
continue;
}
stateByFollower[follower.id] = parsed;
}
return {
ok: errors.length === 0,
stateByFollower,
present,
error: errors.join("; "),
};
}
function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: ParsedOptions, key: string): { ok: boolean; present: boolean; value: string | null; omitted: boolean; error: string } {
const template = `{{ with index .data ${JSON.stringify(key)} }}{{ . }}{{ end }}`;
const maxValueBytes = 32_768;
const script = [
"set -eu",
"tmpdir=$(mktemp -d)",
"cleanup() { rm -rf \"$tmpdir\"; }",
"trap cleanup EXIT INT TERM",
nativeScriptLoadShell(["read-state-summary.mjs"]),
`NAMESPACE=${shQuote(registry.controller.namespace)}`,
`CONFIGMAP=${shQuote(registry.controller.stateConfigMapName)}`,
`MAX_VALUE_BYTES=${maxValueBytes}`,
"export NAMESPACE CONFIGMAP MAX_VALUE_BYTES",
`if ! value=$(kubectl -n "$NAMESPACE" get configmap "$CONFIGMAP" -o go-template=${shQuote(template)} 2>"$tmpdir/error"); then`,
" error_b64=$(tail -c 800 \"$tmpdir/error\" | base64 | tr -d '\\n')",
" if grep -qi 'not found' \"$tmpdir/error\"; then",
" printf '{\"ok\":true,\"present\":false,\"valueB64\":null,\"omitted\":false,\"errorB64\":\"%s\"}' \"$error_b64\"",
" exit 0",
" fi",
" printf '{\"ok\":false,\"present\":false,\"valueB64\":null,\"omitted\":false,\"errorB64\":\"%s\"}' \"$error_b64\"",
" exit 0",
"fi",
"value_bytes=$(printf '%s' \"$value\" | wc -c | tr -d ' ')",
"if [ \"${value_bytes:-0}\" -gt \"$MAX_VALUE_BYTES\" ]; then",
" printf '{\"ok\":true,\"present\":true,\"valueB64\":null,\"omitted\":true,\"valueBytes\":%s,\"errorB64\":\"\"}' \"$value_bytes\"",
" exit 0",
"fi",
"if command -v gzip >/dev/null 2>&1; then",
" value_b64=$(printf '%s' \"$value\" | gzip -c | base64 | tr -d '\\n')",
" printf '{\"ok\":true,\"present\":true,\"valueB64\":\"%s\",\"encoding\":\"gzip-base64\",\"omitted\":false,\"valueBytes\":%s,\"errorB64\":\"\"}' \"$value_b64\" \"$value_bytes\"",
"else",
" value_b64=$(printf '%s' \"$value\" | base64 | tr -d '\\n')",
" printf '{\"ok\":true,\"present\":true,\"valueB64\":\"%s\",\"encoding\":\"base64\",\"omitted\":false,\"valueBytes\":%s,\"errorB64\":\"\"}' \"$value_b64\" \"$value_bytes\"",
"fi",
`FOLLOWERS_JSON=${shQuote(JSON.stringify(registry.followers.map((follower) => follower.id)))}`,
"MAX_TIMING_STAGES=24",
"export NAMESPACE CONFIGMAP FOLLOWERS_JSON MAX_TIMING_STAGES",
"node \"$tmpdir/read-state-summary.mjs\"",
].join("\n");
const result = runKubeScript(registry, options, script, "", 10_000);
const parsed = result.exitCode === 0 ? parseJsonObject(result.stdout) : null;
if (parsed === null) {
return {
ok: false,
present: false,
value: null,
omitted: false,
error: redactText(tailText(result.stderr || result.stdout, 800)),
};
const error = redactText(tailText(result.stderr || result.stdout, 800));
return { ok: false, stateByFollower: {}, present: false, error };
}
const errorB64 = typeof parsed.errorB64 === "string" ? parsed.errorB64 : "";
const error = errorB64.length === 0 ? "" : Buffer.from(errorB64, "base64").toString("utf8");
const ok = parsed.ok === true;
const present = parsed.present === true;
const omitted = parsed.omitted === true;
const valueB64 = typeof parsed.valueB64 === "string" ? parsed.valueB64 : null;
const encoding = stringOrNull(parsed.encoding) ?? "base64";
const valueBuffer = valueB64 === null ? null : Buffer.from(valueB64, "base64");
const parsedStates = asOptionalRecord(parsed.stateByFollower) ?? {};
const stateByFollower: Record<string, Record<string, unknown>> = {};
for (const follower of registry.followers) {
const state = asOptionalRecord(parsedStates[follower.id]);
if (state !== null) stateByFollower[follower.id] = state;
}
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(String).filter((item) => item.length > 0) : [];
return {
ok,
present,
value: valueBuffer === null ? null : encoding === "gzip-base64" ? gunzipSync(valueBuffer).toString("utf8") : valueBuffer.toString("utf8"),
omitted,
error: redactText(error),
ok: parsed.ok === true && errors.length === 0,
stateByFollower,
present: parsed.present === true,
error: errors.join("; "),
};
}