import { execFileSync, spawnSync } from "node:child_process"; function runJson(args) { return JSON.parse(execFileSync("kubectl", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 })); } function duBytes(path) { if (!path || !path.startsWith("/var/lib/rancher/k3s/storage/")) return null; const result = spawnSync("du", ["-sb", path], { encoding: "utf8", timeout: 8000 }); if (result.status !== 0) return null; const value = Number(result.stdout.trim().split(/\s+/u)[0]); return Number.isFinite(value) ? value : null; } const namespace = process.env.NAMESPACE; const confirm = process.env.CONFIRM === "true"; const enabled = process.env.ENABLED === "true"; const limit = Math.max(1, Math.min(Number(process.env.LIMIT || "100"), 1000)); const prefixes = JSON.parse(Buffer.from(process.env.PREFIXES_JSON_B64 || "W10=", "base64").toString("utf8")); if (!enabled) { console.log(JSON.stringify({ ok: false, error: "session-pvc-retention-disabled", selectedPvcCount: 0, mutation: false })); process.exit(0); } if (!namespace || !Array.isArray(prefixes) || prefixes.length === 0) throw new Error("session PVC cleanup requires namespace and YAML prefixes"); const pvData = runJson(["get", "pv", "-o", "json"]); const pvcData = runJson(["-n", namespace, "get", "pvc", "-o", "json"]); const podData = runJson(["-n", namespace, "get", "pod", "-o", "json"]); const pvs = new Map((pvData.items || []).map((pv) => [pv.metadata?.name, pv])); const activeClaims = new Map(); for (const pod of podData.items || []) { const phase = pod.status?.phase; if (phase === "Succeeded" || phase === "Failed") continue; for (const volume of pod.spec?.volumes || []) { const claim = volume.persistentVolumeClaim?.claimName; if (!claim) continue; const list = activeClaims.get(claim) || []; list.push(pod.metadata?.name); activeClaims.set(claim, list); } } const candidates = []; const protectedRows = []; for (const pvc of pvcData.items || []) { const name = pvc.metadata?.name || ""; const matchedPrefix = prefixes.find((prefix) => name.startsWith(prefix)); if (!matchedPrefix) continue; const activeMountPods = activeClaims.get(name) || []; const pv = pvs.get(pvc.spec?.volumeName); const storageClass = pvc.spec?.storageClassName || pv?.spec?.storageClassName || null; const reclaimPolicy = pv?.spec?.persistentVolumeReclaimPolicy || null; const hostPath = pv?.spec?.hostPath?.path || pv?.spec?.local?.path || null; const row = { namespace, pvc: name, volume: pvc.spec?.volumeName || null, matchedPrefix, phase: pvc.status?.phase || null, pvPhase: pv?.status?.phase || null, storageClass, reclaimPolicy, activeMountCount: activeMountPods.length, activeMountPods: activeMountPods.slice(0, 5), estimatedBytes: duBytes(hostPath), }; if (activeMountPods.length > 0 || storageClass !== "local-path" || reclaimPolicy !== "Delete") { protectedRows.push({ ...row, reason: activeMountPods.length > 0 ? "active-mount" : "not-local-path-delete" }); } else { candidates.push(row); } } candidates.sort((a, b) => (b.estimatedBytes || 0) - (a.estimatedBytes || 0)); const selected = candidates.slice(0, limit); const result = { ok: true, planKind: "agentrun-session-pvc-retention", namespace, dryRun: !confirm, mutation: confirm, criteria: { prefixes, storageClass: "local-path", reclaimPolicy: "Delete", requireNoActiveMount: true, limit }, candidatePvcCount: candidates.length, selectedPvcCount: selected.length, protectedPvcCount: protectedRows.length, estimatedReclaimBytes: selected.reduce((sum, item) => sum + (item.estimatedBytes || 0), 0), selectedPreview: selected.slice(0, 12), protectedPreview: protectedRows.slice(0, 12), deletedPvcCount: 0, valuesPrinted: false, }; if (confirm && selected.length > 0) { for (let index = 0; index < selected.length; index += 50) { execFileSync("kubectl", ["-n", namespace, "delete", "pvc", "--wait=false", ...selected.slice(index, index + 50).map((item) => item.pvc)], { encoding: "utf8", maxBuffer: 1024 * 1024 }); } result.deletedPvcCount = selected.length; result.deleteMode = "submit-only-wait-false"; } console.log(JSON.stringify(result));