Files
pikasTech-unidesk/scripts/native/cicd/read-state-summary.mjs
T

198 lines
6.8 KiB
JavaScript

import { readFileSync } from "node:fs";
import https from "node:https";
const namespace = process.env.NAMESPACE || "";
const configMap = process.env.CONFIGMAP || "";
const followerIds = parseFollowerIds(process.env.FOLLOWERS_JSON || "[]");
const maxTimingStages = Number(process.env.MAX_TIMING_STAGES || "24");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function parseFollowerIds(text) {
try {
const parsed = JSON.parse(text);
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.length > 0) : [];
} catch {
return [];
}
}
function request(method, path, body, contentType = "application/json") {
return new Promise((resolve, reject) => {
const headers = { authorization: `Bearer ${token}` };
const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body);
if (payload !== null) {
headers["content-type"] = contentType;
headers["content-length"] = Buffer.byteLength(payload);
}
const req = https.request({ host, port, path, method, ca, headers }, (res) => {
let text = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { text += chunk; });
res.on("end", () => resolve({ status: res.statusCode || 0, text }));
});
req.on("error", reject);
if (payload !== null) req.write(payload);
req.end();
});
}
async function readConfigMap() {
try {
const result = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps/${encodeURIComponent(configMap)}`);
if (result.status === 404) return { ok: true, present: false, object: null, error: result.text };
if (result.status < 200 || result.status >= 300) return { ok: false, present: false, object: null, error: result.text || `kube api GET configmap status ${result.status}` };
return { ok: true, present: true, object: JSON.parse(result.text), error: "" };
} catch (error) {
return { ok: false, present: false, object: null, error: error?.message || String(error) };
}
}
function compactStateText(text) {
if (typeof text !== "string" || text.length === 0) return null;
let state;
try {
state = JSON.parse(text);
} catch {
return null;
}
return {
id: stringOrNull(state.id),
adapter: stringOrNull(state.adapter),
enabled: state.enabled === true,
phase: stringOrNull(state.phase),
source: compactSource(state.source),
target: compactTarget(state.target),
lastTriggeredSha: stringOrNull(state.lastTriggeredSha),
lastSucceededSha: stringOrNull(state.lastSucceededSha),
pipelineRun: stringOrNull(state.pipelineRun),
inFlightJob: stringOrNull(state.inFlightJob),
controller: compactController(state.controller),
decision: stringOrNull(state.decision),
dryRun: state.dryRun === true,
updatedAt: stringOrNull(state.updatedAt),
timings: compactTimings(state.timings),
warnings: arrayStrings(state.warnings).slice(0, 6),
stateFormat: stringOrNull(state.stateFormat),
};
}
function compactSource(source) {
const value = recordOrNull(source);
if (value === null) return null;
return {
repository: stringOrNull(value.repository),
branch: stringOrNull(value.branch),
branchRef: stringOrNull(value.branchRef),
snapshotPrefix: stringOrNull(value.snapshotPrefix),
observedSha: stringOrNull(value.observedSha),
};
}
function compactTarget(target) {
const value = recordOrNull(target);
if (value === null) return null;
return {
node: stringOrNull(value.node),
lane: stringOrNull(value.lane),
namespace: stringOrNull(value.namespace),
sentinel: stringOrNull(value.sentinel),
targetSha: stringOrNull(value.targetSha),
};
}
function compactController(controller) {
const value = recordOrNull(controller);
if (value === null) return null;
return {
mode: stringOrNull(value.mode),
stateConfigMap: stringOrNull(value.stateConfigMap),
leaseName: stringOrNull(value.leaseName),
};
}
function compactTimings(timings) {
const value = recordOrNull(timings);
if (value === null) return null;
return {
budgetSeconds: numberOrNull(value.budgetSeconds),
totalSeconds: numberOrNull(value.totalSeconds),
totalStatus: stringOrNull(value.totalStatus),
totalSource: stringOrNull(value.totalSource),
sourceCommit: stringOrNull(value.sourceCommit),
startedAt: stringOrNull(value.startedAt),
finishedAt: stringOrNull(value.finishedAt),
overBudget: typeof value.overBudget === "boolean" ? value.overBudget : null,
stages: arrayRecords(value.stages).slice(0, maxTimingStages).map(compactStageTiming),
};
}
function compactStageTiming(stage) {
return {
stage: stringOrNull(stage.stage),
status: stringOrNull(stage.status),
seconds: numberOrNull(stage.seconds),
budgetSeconds: numberOrNull(stage.budgetSeconds),
source: stringOrNull(stage.source),
object: stringOrNull(stage.object),
};
}
function recordOrNull(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
}
function arrayRecords(value) {
return Array.isArray(value) ? value.filter((item) => recordOrNull(item) !== null) : [];
}
function arrayStrings(value) {
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
}
function stringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberOrNull(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
const result = await readConfigMap();
const errors = [];
const stateByFollower = {};
const valueBytes = {};
if (result.ok && result.present) {
const data = recordOrNull(result.object?.data) || {};
for (const id of followerIds) {
const text = typeof data[id] === "string" ? data[id] : "";
if (text.length === 0) continue;
valueBytes[id] = Buffer.byteLength(text, "utf8");
const compact = compactStateText(text);
if (compact === null) errors.push(`${id}: invalid state json`);
else stateByFollower[id] = compact;
}
}
if (!result.ok) errors.push(result.error);
process.stdout.write(JSON.stringify({
ok: result.ok && errors.length === 0,
present: result.present,
metadata: result.object === null ? null : {
name: stringOrNull(result.object?.metadata?.name),
namespace: stringOrNull(result.object?.metadata?.namespace),
resourceVersion: stringOrNull(result.object?.metadata?.resourceVersion),
updatedAt: stringOrNull(result.object?.data?._updatedAt),
keyCount: Object.keys(recordOrNull(result.object?.data) || {}).length,
},
stateByFollower,
valueBytes,
errors,
statusAuthority: "target-node-summary",
parsedDownstreamCliOutput: false,
}));