import { existsSync, readFileSync } from "node:fs"; import https from "node:https"; import { spawnSync } from "node:child_process"; const namespace = requiredEnv("RUNTIME_NAMESPACE"); const expectedSha = process.env.EXPECTED_SHA || ""; const workloads = parseWorkloads(requiredEnv("WORKLOADS_B64")); const maxMessageBytes = positiveInt(process.env.MAX_MESSAGE_BYTES, "MAX_MESSAGE_BYTES"); const maxContainers = positiveInt(process.env.MAX_CONTAINERS, "MAX_CONTAINERS"); const useServiceAccount = Boolean(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT) && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token") && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); class KubeReadError extends Error { constructor(reason, message, details = {}) { super(shortText(message) || reason); this.name = "KubeReadError"; this.reason = reason; this.statusCode = details.statusCode ?? null; this.notFound = details.notFound === true; this.path = details.path || null; } } main().catch((error) => { process.stderr.write(error?.message || String(error)); process.exit(1); }); async function main() { const items = []; for (const spec of workloads) { items.push(await workloadSummary(spec)); } const targetShas = items.map((item) => item.sourceCommit?.value).filter(Boolean); const targetSha = uniqueOrNull(targetShas); const ready = items.length > 0 && items.every((item) => item.ready === true); const aligned = expectedSha ? targetShas.length > 0 && targetShas.every((value) => value === expectedSha) : null; console.log(JSON.stringify({ ok: items.every((item) => item.ok !== false), degradedReason: items.some((item) => item.ok === false) ? "runtime-read-degraded" : null, namespace, expectedSha: shortSha(expectedSha), targetSha: shortSha(targetSha), ready, aligned, firstSeenReadyAt: null, firstSeenExpectedShaAt: null, lastObservedAt: new Date().toISOString(), blockingReason: blockingReason(items, expectedSha), workloads: items, statusAuthority: useServiceAccount ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw", parsedDownstreamCliOutput: false, })); } async function workloadSummary(spec) { const resource = await getJson(apiPathForWorkload(spec), false); if (resource === null) { return { ok: false, degradedReason: "workload-not-found", kind: spec.kind, name: spec.name, namespace, message: `${spec.kind} ${namespace}/${spec.name} was not found`, ready: false, aligned: expectedSha ? false : null, sourceCommit: null, pods: [], }; } const selector = matchLabels(resource.spec?.selector?.matchLabels); const pods = selector === null ? [] : await listPods(selector); const sourceCommit = sourceCommitSummary(spec, resource, pods); const ready = workloadReady(spec.kind, resource); return { ok: true, kind: spec.kind, name: spec.name, namespace, generation: integerOrNull(resource.metadata?.generation), observedGeneration: integerOrNull(resource.status?.observedGeneration), replicas: integerOrNull(resource.status?.replicas), updatedReplicas: integerOrNull(resource.status?.updatedReplicas), readyReplicas: integerOrNull(resource.status?.readyReplicas), availableReplicas: integerOrNull(resource.status?.availableReplicas), unavailableReplicas: integerOrNull(resource.status?.unavailableReplicas), ready, aligned: expectedSha && sourceCommit.value ? sourceCommit.value === expectedSha : null, selector, conditions: arrayItems(resource.status?.conditions).slice(-4).map(compactCondition), sourceCommit, pods: pods.slice(0, 4).map((pod) => podSummary(spec, pod)), }; } function apiPathForWorkload(spec) { const plural = spec.kind === "StatefulSet" ? "statefulsets" : "deployments"; return `/apis/apps/v1/namespaces/${encodeURIComponent(namespace)}/${plural}/${encodeURIComponent(spec.name)}`; } async function listPods(selector) { const labelSelector = Object.entries(selector).map(([key, value]) => `${key}=${value}`).join(","); const list = await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${encodeURIComponent(labelSelector)}`, false); return arrayItems(list?.items).sort((left, right) => String(right.metadata?.creationTimestamp || "").localeCompare(String(left.metadata?.creationTimestamp || ""))); } function podSummary(spec, pod) { const readyCondition = conditionByType(pod.status, "Ready"); const sourceCommit = podSourceCommitSummary(spec, pod); return { name: pod.metadata?.name || null, phase: pod.status?.phase || null, createdAt: pod.metadata?.creationTimestamp || null, startTime: pod.status?.startTime || null, ready: readyCondition?.status || null, readyReason: readyCondition?.reason || null, readyMessage: shortText(readyCondition?.message || null), readyLastTransitionTime: readyCondition?.lastTransitionTime || null, sourceCommit, containers: allContainerStatuses(pod).slice(0, maxContainers).map(containerSummary), }; } function sourceCommitSummary(spec, resource, pods) { const checks = []; for (const key of arrayItems(spec.sourceCommit?.labels)) checks.push({ source: "workload-label", key, value: resource.metadata?.labels?.[key] || null }); for (const key of arrayItems(spec.sourceCommit?.annotations)) checks.push({ source: "workload-annotation", key, value: resource.metadata?.annotations?.[key] || null }); const template = resource.spec?.template?.metadata || {}; for (const key of arrayItems(spec.sourceCommit?.podLabels)) checks.push({ source: "template-label", key, value: template.labels?.[key] || null }); for (const key of arrayItems(spec.sourceCommit?.podAnnotations)) checks.push({ source: "template-annotation", key, value: template.annotations?.[key] || null }); const podCommit = pods.map((pod) => podSourceCommitSummary(spec, pod).value).find(Boolean) || null; if (podCommit) checks.push({ source: "selected-pod", key: "pod", value: podCommit }); const found = checks.find((item) => item.value); return { value: found?.value || null, short: shortSha(found?.value || null), source: found?.source || null, key: found?.key || null, checks: checks.map((item) => ({ source: item.source, key: item.key, short: shortSha(item.value) })), }; } function podSourceCommitSummary(spec, pod) { const checks = []; for (const key of arrayItems(spec.sourceCommit?.podLabels)) checks.push({ source: "pod-label", key, value: pod.metadata?.labels?.[key] || null }); for (const key of arrayItems(spec.sourceCommit?.podAnnotations)) checks.push({ source: "pod-annotation", key, value: pod.metadata?.annotations?.[key] || null }); for (const envName of arrayItems(spec.sourceCommit?.env)) { const value = envValue(pod, envName); checks.push({ source: "pod-env", key: envName, value }); } const found = checks.find((item) => item.value); return { value: found?.value || null, short: shortSha(found?.value || null), source: found?.source || null, key: found?.key || null, checks: checks.map((item) => ({ source: item.source, key: item.key, short: shortSha(item.value) })), }; } function envValue(pod, envName) { for (const container of arrayItems(pod.spec?.containers)) { for (const env of arrayItems(container.env)) { if (env.name === envName && typeof env.value === "string") return env.value; if (env.name === envName && env.valueFrom) return null; } } return null; } function workloadReady(kind, resource) { const status = resource.status || {}; if (kind === "Deployment") { const desired = integerOrNull(resource.spec?.replicas) ?? 1; const available = integerOrNull(status.availableReplicas) ?? 0; const updated = integerOrNull(status.updatedReplicas) ?? 0; return available >= desired && updated >= desired; } const desired = integerOrNull(resource.spec?.replicas) ?? 1; const ready = integerOrNull(status.readyReplicas) ?? 0; return ready >= desired; } function blockingReason(items, expected) { const missing = items.find((item) => item.ok === false); if (missing) return `${missing.kind}/${missing.name} not-found`; const notReady = items.find((item) => item.ready !== true); if (notReady) return `${notReady.kind}/${notReady.name} not-ready`; if (expected) { const stale = items.find((item) => item.sourceCommit?.value && item.sourceCommit.value !== expected); if (stale) return `${stale.kind}/${stale.name} source-mismatch`; } return null; } async function getJson(path, required) { let text = ""; try { text = await getText(path, required); } catch (error) { if (!required && isNotFoundError(error)) return null; throw error; } try { return JSON.parse(text); } catch (error) { throw new KubeReadError("invalid-json", `kube api returned non-JSON for ${path}: ${error?.message || String(error)}`, { path }); } } async function getText(path, required) { if (useServiceAccount) return kubeApiGet(path, required); const result = spawnSync("kubectl", ["get", "--raw", path], { encoding: "utf8", maxBuffer: 1024 * 1024 }); if (result.error) throw new KubeReadError("transport-error", `kubectl get --raw transport error for ${path}: ${result.error.message}`, { path }); if (result.status === 0) return result.stdout; const body = result.stderr || result.stdout || `kubectl get --raw failed with exit ${result.status}`; throw new KubeReadError(isNotFoundText(body) ? "not-found" : "kube-api-error", `kubectl get --raw failed for ${path}: ${body}`, { path, notFound: isNotFoundText(body), }); } function kubeApiGet(path, required) { 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"); return new Promise((resolve, reject) => { const req = https.request({ host, port, path, method: "GET", ca, headers: { authorization: `Bearer ${token}` } }, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { body += chunk; }); res.on("end", () => { const code = res.statusCode || 0; if (code >= 200 && code < 300) resolve(body); else reject(new KubeReadError(code === 404 || isNotFoundText(body) ? "not-found" : "kube-api-error", `kube api GET ${path} status ${code}: ${body || "-"}`, { path, statusCode: code, notFound: code === 404 || isNotFoundText(body), })); }); }); req.on("error", (error) => reject(new KubeReadError("transport-error", `kube api GET ${path} transport error: ${error?.message || String(error)}`, { path }))); req.end(); }); } function parseWorkloads(value) { try { const parsed = JSON.parse(Buffer.from(value, "base64").toString("utf8")); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function matchLabels(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0); return entries.length === 0 ? null : Object.fromEntries(entries); } function allContainerStatuses(pod) { const status = pod?.status || {}; return [ ...arrayItems(status.initContainerStatuses), ...arrayItems(status.containerStatuses), ...arrayItems(status.ephemeralContainerStatuses), ]; } function containerSummary(item) { return { name: item.name || null, ready: item.ready === true, restartCount: integerOrNull(item.restartCount), waiting: compactWaiting(item.state?.waiting), running: item.state?.running ? { startedAt: item.state.running.startedAt || null } : null, terminated: compactTerminated(item.state?.terminated || item.lastState?.terminated), imageID: shortImageId(item.imageID || null), }; } function conditionByType(status, type) { return arrayItems(status?.conditions).find((item) => item?.type === type) || null; } function compactCondition(value) { if (!value) return null; return { type: value.type || null, status: value.status || null, reason: value.reason || null, message: shortText(value.message || null), lastTransitionTime: value.lastTransitionTime || null }; } function compactTerminated(value) { if (!value) return null; return { reason: value.reason || null, exitCode: integerOrNull(value.exitCode), message: shortText(value.message || null), startedAt: value.startedAt || null, finishedAt: value.finishedAt || null }; } function compactWaiting(value) { if (!value) return null; return { reason: value.reason || null, message: shortText(value.message || null) }; } function uniqueOrNull(values) { const unique = [...new Set(values.filter(Boolean))]; return unique.length === 1 ? unique[0] : null; } function shortImageId(value) { if (!value) return null; const text = String(value); const at = text.lastIndexOf("@"); return at >= 0 ? text.slice(at + 1, at + 25) : shortText(text); } function shortSha(value) { if (!value) return null; const text = String(value); return text.length > 12 ? text.slice(0, 12) : text; } function shortText(value) { if (value === null || value === undefined) return null; const text = String(value).replace(/\s+/gu, " ").trim(); return text.length <= maxMessageBytes ? text : `${text.slice(0, Math.max(0, maxMessageBytes - 3))}...`; } function arrayItems(value) { return Array.isArray(value) ? value : []; } function integerOrNull(value) { return Number.isInteger(value) ? value : null; } function isNotFoundError(error) { return error instanceof KubeReadError && error.notFound === true; } function isNotFoundText(value) { return /\b404\b|not found|NotFound/u.test(String(value || "")); } function positiveInt(value, name) { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`); return parsed; } function requiredEnv(name) { const value = process.env[name]; if (!value) throw new Error(`${name} is required`); return value; }