413 lines
15 KiB
JavaScript
413 lines
15 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync } 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 tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
const caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
|
|
const inCluster = Boolean(process.env.KUBERNETES_SERVICE_HOST && existsSync(tokenPath) && existsSync(caPath));
|
|
const host = process.env.KUBERNETES_SERVICE_HOST;
|
|
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
|
|
const token = inCluster ? readFileSync(tokenPath, "utf8").trim() : "";
|
|
const ca = inCluster ? readFileSync(caPath) : null;
|
|
|
|
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() {
|
|
if (!inCluster) return readConfigMapViaKubectl();
|
|
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 readConfigMapViaKubectl() {
|
|
try {
|
|
const stdout = execFileSync("kubectl", ["-n", namespace, "get", "configmap", configMap, "-o", "json"], {
|
|
encoding: "utf8",
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
return { ok: true, present: true, object: JSON.parse(stdout), error: "" };
|
|
} catch (error) {
|
|
const stderr = String(error?.stderr || error?.message || "");
|
|
if (/not found/i.test(stderr)) return { ok: true, present: false, object: null, error: stderr };
|
|
return { ok: false, present: false, object: null, error: stderr || "kubectl configmap read failed" };
|
|
}
|
|
}
|
|
|
|
function compactStateText(text, includeCommand) {
|
|
if (typeof text !== "string" || text.length === 0) return null;
|
|
let state;
|
|
try {
|
|
state = JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
const compact = {
|
|
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),
|
|
};
|
|
if (includeCommand) compact.command = compactCommand(state.command);
|
|
return compact;
|
|
}
|
|
|
|
function compactCommand(command) {
|
|
const value = recordOrNull(command);
|
|
if (value === null) return null;
|
|
return {
|
|
mode: stringOrNull(value.mode) ?? stringOrNull(value.status),
|
|
namespace: stringOrNull(value.namespace),
|
|
pipelineRun: stringOrNull(value.pipelineRun),
|
|
sourceCommit: stringOrNull(value.sourceCommit),
|
|
sourceStageRef: stringOrNull(value.sourceStageRef),
|
|
wait: value.wait === true ? true : null,
|
|
pipelineRunCompleted: value.pipelineRunCompleted === true ? true : null,
|
|
stillRunning: value.stillRunning === true ? true : null,
|
|
closeout: compactCloseout(value.closeout),
|
|
payload: compactNativePayload(value.payload),
|
|
exitCode: numberOrNull(value.exitCode),
|
|
timedOut: value.timedOut === true,
|
|
statusAuthority: stringOrNull(value.statusAuthority),
|
|
parsedDownstreamCliOutput: false,
|
|
};
|
|
}
|
|
|
|
function compactCloseout(closeout) {
|
|
const value = recordOrNull(closeout);
|
|
if (value === null) return null;
|
|
return {
|
|
ok: value.ok === true,
|
|
completed: value.completed === true,
|
|
timedOut: value.timedOut === true,
|
|
polls: numberOrNull(value.polls),
|
|
elapsedMs: numberOrNull(value.elapsedMs),
|
|
summary: compactNativePayload(value.summary),
|
|
statusAuthority: stringOrNull(value.statusAuthority),
|
|
parsedDownstreamCliOutput: false,
|
|
};
|
|
}
|
|
|
|
function compactNativePayload(payload) {
|
|
const value = recordOrNull(payload);
|
|
if (value === null) return null;
|
|
return {
|
|
gitMirror: compactGitMirror(value.gitMirror),
|
|
tekton: compactTekton(value.tekton),
|
|
taskRuns: compactTaskRuns(value.taskRuns),
|
|
planArtifacts: compactPlanArtifacts(value.planArtifacts),
|
|
argo: compactArgo(value.argo),
|
|
runtime: compactRuntime(value.runtime),
|
|
errors: arrayStrings(value.errors).slice(0, 5),
|
|
statusAuthority: stringOrNull(value.statusAuthority),
|
|
parsedDownstreamCliOutput: false,
|
|
};
|
|
}
|
|
|
|
function compactGitMirror(gitMirror) {
|
|
const value = recordOrNull(gitMirror);
|
|
if (value === null) return null;
|
|
return {
|
|
ok: value.ok === true,
|
|
sourceSnapshotReady: value.sourceSnapshotReady === true,
|
|
pendingFlush: value.pendingFlush === true,
|
|
githubInSync: value.githubInSync === true,
|
|
sourceBranch: stringOrNull(value.sourceBranch),
|
|
gitopsBranch: stringOrNull(value.gitopsBranch),
|
|
localSource: stringOrNull(value.localSource),
|
|
githubSource: stringOrNull(value.githubSource),
|
|
localGitops: stringOrNull(value.localGitops),
|
|
githubGitops: stringOrNull(value.githubGitops),
|
|
};
|
|
}
|
|
|
|
function compactTekton(tekton) {
|
|
const value = recordOrNull(tekton);
|
|
if (value === null) return null;
|
|
return {
|
|
name: stringOrNull(value.name),
|
|
succeeded: value.succeeded === true ? true : value.succeeded === false ? false : null,
|
|
reason: stringOrNull(value.reason),
|
|
startTime: stringOrNull(value.startTime),
|
|
completionTime: stringOrNull(value.completionTime),
|
|
durationSeconds: numberOrNull(value.durationSeconds),
|
|
};
|
|
}
|
|
|
|
function compactArgo(argo) {
|
|
const value = recordOrNull(argo);
|
|
if (value === null) return null;
|
|
return {
|
|
name: stringOrNull(value.name),
|
|
syncStatus: stringOrNull(value.syncStatus),
|
|
healthStatus: stringOrNull(value.healthStatus),
|
|
healthMessage: stringOrNull(value.healthMessage),
|
|
revision: stringOrNull(value.revision),
|
|
operationPhase: stringOrNull(value.operationPhase),
|
|
operationMessage: stringOrNull(value.operationMessage),
|
|
operationStartedAt: stringOrNull(value.operationStartedAt),
|
|
operationFinishedAt: stringOrNull(value.operationFinishedAt),
|
|
operationDurationSeconds: numberOrNull(value.operationDurationSeconds),
|
|
conditions: arrayRecords(value.conditions).slice(0, 5),
|
|
nonReadyResources: arrayRecords(value.nonReadyResources).slice(0, 5),
|
|
ready: value.ready === true,
|
|
};
|
|
}
|
|
|
|
function compactRuntime(runtime) {
|
|
const value = recordOrNull(runtime);
|
|
if (value === null) return null;
|
|
return {
|
|
namespace: stringOrNull(value.namespace),
|
|
ready: value.ready === true,
|
|
targetSha: stringOrNull(value.targetSha),
|
|
expectedSha: stringOrNull(value.expectedSha),
|
|
aligned: value.aligned === true ? true : value.aligned === false ? false : null,
|
|
};
|
|
}
|
|
|
|
function compactTaskRuns(taskRuns) {
|
|
const value = recordOrNull(taskRuns);
|
|
if (value === null) return null;
|
|
const items = arrayRecords(value.items);
|
|
return {
|
|
ok: value.ok === true,
|
|
count: numberOrNull(value.count),
|
|
succeededCount: numberOrNull(value.succeededCount),
|
|
failedCount: numberOrNull(value.failedCount),
|
|
activeCount: numberOrNull(value.activeCount),
|
|
performance: recordOrNull(value.performance),
|
|
failedItems: items.filter((item) => item.status === "False").slice(0, 5).map(compactTaskRunItem),
|
|
activeItems: items.filter((item) => item.status !== "True" && item.status !== "False").slice(0, 5).map(compactTaskRunItem),
|
|
slowItems: arrayRecords(recordOrNull(value.performance)?.slowTaskRuns).slice(0, 5).map(compactTaskRunItem),
|
|
};
|
|
}
|
|
|
|
function compactTaskRunItem(item) {
|
|
return {
|
|
name: stringOrNull(item.name),
|
|
pipelineTask: stringOrNull(item.pipelineTask),
|
|
status: stringOrNull(item.status),
|
|
reason: stringOrNull(item.reason),
|
|
durationSeconds: numberOrNull(item.durationSeconds),
|
|
};
|
|
}
|
|
|
|
function compactPlanArtifacts(planArtifacts) {
|
|
const value = recordOrNull(planArtifacts);
|
|
if (value === null) return null;
|
|
return {
|
|
ok: value.ok === true,
|
|
pipelineRun: stringOrNull(value.pipelineRun),
|
|
eventFound: value.eventFound === true,
|
|
degradedReason: stringOrNull(value.degradedReason),
|
|
sourceCommitId: stringOrNull(value.sourceCommitId),
|
|
affectedServicesCount: arrayStrings(value.affectedServices).length,
|
|
rolloutServicesCount: arrayStrings(value.rolloutServices).length,
|
|
buildServicesCount: arrayStrings(value.buildServices).length,
|
|
reusedServicesCount: arrayStrings(value.reusedServices).length,
|
|
buildSkippedCount: numberOrNull(value.buildSkippedCount),
|
|
summary: stringOrNull(value.summary),
|
|
disclosure: stringOrNull(value.disclosure),
|
|
};
|
|
}
|
|
|
|
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: prioritizedStageTimings(arrayRecords(value.stages)).slice(0, maxTimingStages).map(compactStageTiming),
|
|
};
|
|
}
|
|
|
|
function prioritizedStageTimings(stages) {
|
|
const priority = [];
|
|
const rest = [];
|
|
for (const stage of stages) {
|
|
if (isPriorityTaskStage(stage)) priority.push(stage);
|
|
else rest.push(stage);
|
|
}
|
|
const seen = new Set();
|
|
const out = [];
|
|
for (const stage of [...priority, ...rest]) {
|
|
const key = [
|
|
stringOrNull(stage.stage),
|
|
stringOrNull(stage.status),
|
|
stringOrNull(stage.source),
|
|
stringOrNull(stage.object),
|
|
].filter((item) => item !== null).join("|");
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
out.push(stage);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function isPriorityTaskStage(stage) {
|
|
const name = stringOrNull(stage.stage) || "";
|
|
if (!name.startsWith("task:")) return false;
|
|
const status = stringOrNull(stage.status) || "";
|
|
const seconds = numberOrNull(stage.seconds);
|
|
return status.startsWith("failed") || status === "running" || (seconds !== null && seconds > 60);
|
|
}
|
|
|
|
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) || {};
|
|
const includeCommand = followerIds.length === 1;
|
|
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, includeCommand);
|
|
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,
|
|
}));
|