/* * SysDeck - Kata Panel (v0.0.43) * Author: Jeremy Anderson (https://dcos.net) * * v0.0.43 PRODUCTION REWRITE — PREVIOUS VERSION WAS MOCK DATA. * * The v0.0.35-v0.0.43 Kata panel shipped a pre-built React bundle * from the upstream cockpit-kata sub-project. That bundle displayed * HARDCODED MOCK DATA: * - 5 fake sandboxes (web-frontend-prod, api-gateway-staging, etc.) * with synthetic UUIDs and createdAt:"2026-07-15..." timestamps * - fake per-sandbox metrics (cpuUsagePercent, memoryUsageMB, * historyCpu/historyMemory arrays) * - a fake QCrows bundle catalog * - a fake PXE status (always dnsmasqRunning:true) * The only real features were the QCrows kernel-bundle extraction * (qcrows-export / qcrows-initrd-regen via cockpit.spawn) and the * kata-runtime check call. * * v0.0.43 deletes the React bundle and ships this vanilla-JS panel * backed by bridge/kata.py. Every value displayed is REAL: * - Sandbox list comes from kata-monitor /sandboxes + filesystem * enumeration of /run/vc/sbs/ (Go shim) + /run/kata/ (Rust shim). * - Per-sandbox metrics come from kata-monitor /metrics?sandbox= * (Prometheus text, parsed). * - Runtime version comes from `kata-runtime version` + `kata-runtime * env --json`. * - Host capability comes from `kata-runtime check` (exit code). * - PXE status comes from `systemctl is-active dnsmasq` + real * filesystem probes of /srv/tftp/. * - QCrows bundle list comes from real filesystem enumeration of * /usr/share/sysdeck/kata/qcrows/. * * When no sandboxes are running, the panel shows an EMPTY STATE * (not mock data). When kata-runtime is not installed, the panel * shows an install hint. When kata-monitor is not running, the * metrics card shows a hint to start it. * * Security hardening (v0.0.36 + v0.0.43): * - Sandbox IDs validated with ^[0-9a-f]{64}$ in the bridge before * any subprocess or HTTP call. CVE-2024-2947 lesson. * - All bridge output rendered with escapeHtml() / textContent. * CVE-2022-36446 lesson. * - No innerHTML on bridge data. * - HTTP to kata-monitor is 127.0.0.1-only, no redirects (SSRF * defense). CVE-2020-35850 lesson. */ export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); // Load summary + pxe-status + qcrows-list in parallel. const [summary, pxeStatus, qcrowsList] = await Promise.all([ safe(bridge.kata.summary(), { total_sandboxes: 0, running_sandboxes: 0, sandboxes: [], kata_monitor: { running: false }, kata_runtime: { installed: false }, host_capable: false, check_message: 'unknown', }), safe(bridge.kata.pxeStatus(), { dnsmasq_running: false, tftp_dir_exists: false, tftp_dir_writable: false, pxelinux_entries: [], }), safe(bridge.kata.qcrowsList(), []), ]); panel.innerHTML = `

SysDeck Kata

Kata Containers — hardware-virtualized OCI sandboxes · ${summary.host_capable ? 'host capable' : 'host not capable'} · ${summary.total_sandboxes} sandbox${summary.total_sandboxes === 1 ? '' : 'es'} ${summary.running_sandboxes !== summary.total_sandboxes ? ` (${summary.running_sandboxes} running)` : ''} ${summary.kata_runtime?.installed ? ` · kata-runtime ${escapeHtml(summary.kata_runtime.version || '?')}` : ' · kata-runtime not installed'} ${summary.kata_monitor?.running ? ' · kata-monitor running' : ' · kata-monitor not running'}

${renderRuntimeCard(summary)} ${renderSandboxList(summary.sandboxes, summary.kata_monitor)} ${renderPxeCard(pxeStatus)} ${renderQcrowsCard(qcrowsList)} `; wireEvents(panel, { bridge, EventBus }); EventBus.emit('kata.loaded', { totalSandboxes: summary.total_sandboxes, runningSandboxes: summary.running_sandboxes, hostCapable: summary.host_capable, }); } // ── Render helpers ────────────────────────────────────────────────── function renderRuntimeCard(summary) { const rt = summary.kata_runtime || {}; if (!rt.installed) { return `

Runtime

kata-runtime is not installed. Install Kata Containers 3.x:

# Arch (AUR):
yay -S kata-runtime kata-containers-image

# Debian/Ubuntu (official repo):
sudo apt install kata-runtime kata-containers-image

# Or build from source (you mentioned compiling yesterday):
# https://github.com/kata-containers/kata-containers/blob/main/docs/install/

After install, run kata-runtime check to verify host capability (nested virt, KVM, etc.).

`; } const env = rt.env || {}; const host = env.Host || {}; const hypervisor = env.Hypervisor || {}; return `

Runtime

${host.Kernel ? `` : ''} ${host.Architecture ? `` : ''} ${hypervisor.Path ? `` : ''} ${hypervisor.MachineType ? `` : ''}
kata-runtime version${escapeHtml(rt.version || '?')}
commit${escapeHtml(rt.commit || '?')}
OCI specs${escapeHtml(rt.oci || '?')}
host capable${summary.host_capable ? 'yes' : 'no'}
check message${escapeHtml(summary.check_message || '')}
host kernel${escapeHtml(host.Kernel)}
architecture${escapeHtml(host.Architecture)}
hypervisor${escapeHtml(hypervisor.Path)}
machine type${escapeHtml(hypervisor.MachineType)}
`; } function renderSandboxList(sandboxes, kataMonitor) { if (!sandboxes || !sandboxes.length) { return `

Sandboxes (0)

No kata sandboxes running. This is the real empty state — not mock data. Sandboxes are enumerated from:

  • kata-monitor /sandboxes (HTTP, port 8090)${kataMonitor?.running ? ' ✓ running' : ' — not running'}
  • /run/vc/sbs/<id>/ (Go shim filesystem)
  • /run/kata/<id>/ (Rust shim filesystem)

To create a sandbox, use ctr, crictl, or kubectl with RuntimeClass kata.

`; } const rows = sandboxes.map((sb) => { const idShort = sb.id.substring(0, 12); const isRunning = sb.agent_url || sb.shim_socket; return ` ${escapeHtml(idShort)}… ${isRunning ? 'running' : 'unknown'} ${escapeHtml(sb.source || '?')} ${sb.agent_url ? escapeHtml(sb.agent_url) : '—'} `; }).join(''); return `

Sandboxes (${sandboxes.length})

${rows}
IDStatusSourceAgent URLActions
`; } function renderPxeCard(pxe) { const dnsmasqBadge = pxe.dnsmasq_running ? 'running' : 'not running'; const tftpExistsBadge = pxe.tftp_dir_exists ? 'exists' : 'missing'; const tftpWritableBadge = pxe.tftp_dir_writable ? 'writable' : 'not writable'; const entries = (pxe.pxelinux_entries || []).length ? pxe.pxelinux_entries.map((e) => `${escapeHtml(e)}`).join('') : '(no entries)'; return `

PXE / TFTP Boot

dnsmasq${dnsmasqBadge}
/srv/tftp${tftpExistsBadge} ${tftpWritableBadge}
pxelinux.cfg/ entries${entries}

PXE boot configuration for network-booting kata sandboxes. dnsmasq serves DHCP + TFTP; pxelinux.cfg/ holds per-host boot configs (named by MAC address or "default").

`; } function renderQcrowsCard(qcrows) { if (!qcrows || !qcrows.length) { return `

QCrows Kernel Bundles (0)

No QCrows kernel bundles found at /usr/share/sysdeck/kata/qcrows/. This is the real empty state — not mock data.

QCrows bundles are pre-built kata kernel + initrd + rootfs images. Build one with:

qcrows-export --kernel /path/to/vmlinuz --initrd /path/to/initrd \\
  --rootfs /path/to/rootfs --name alpine-3.20-kata
`; } const totalSize = qcrows.reduce((sum, q) => sum + (q.size_bytes || 0), 0); const totalMb = (totalSize / (1024 * 1024)).toFixed(1); const rows = qcrows.map((q) => ` ${escapeHtml(q.filename)} ${q.size_mb} MB ${new Date(q.mtime * 1000).toISOString().split('T')[0]} `).join(''); return `

QCrows Kernel Bundles (${qcrows.length}, ${totalMb} MB total)

${rows}
FilenameSizeModified
`; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { const output = (msg, isError = false) => { const card = panel.querySelector('#kata-output'); const pre = panel.querySelector('#kata-output-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = msg; pre.style.color = isError ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)'; }; panel.querySelector('#btn-kata-output-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-output'); if (card) card.style.display = 'none'; }); // Refresh buttons (multiple — runtime card + sandbox card). panel.querySelectorAll('#btn-kata-refresh, #btn-kata-pxe-refresh').forEach((btn) => { btn.addEventListener('click', () => mount(panel, { bridge, EventBus })); }); // Inspect buttons. panel.querySelectorAll('.btn-kata-inspect').forEach((btn) => { btn.addEventListener('click', async () => { const sid = btn.dataset.id; const card = panel.querySelector('#kata-inspect-card'); const pre = panel.querySelector('#kata-inspect-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = `Inspecting ${sid.substring(0, 12)}…`; try { const r = await bridge.kata.inspect(sid); pre.textContent = JSON.stringify(r, null, 2); } catch (err) { pre.textContent = `Inspect error: ${err.message || err}`; } }); }); panel.querySelector('#btn-kata-inspect-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-inspect-card'); if (card) card.style.display = 'none'; }); // Metrics buttons. panel.querySelectorAll('.btn-kata-metrics').forEach((btn) => { btn.addEventListener('click', async () => { const sid = btn.dataset.id; const card = panel.querySelector('#kata-metrics-card'); const pre = panel.querySelector('#kata-metrics-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = `Fetching metrics for ${sid.substring(0, 12)}…`; try { const r = await bridge.kata.metrics(sid); if (r.error) { pre.textContent = `Metrics error: ${r.error}`; } else if (r.parsed && r.summary) { const s = r.summary; pre.textContent = [ `Sandbox: ${r.id}`, `CPU: ${s.cpu_usage_percent ?? 'n/a'}%`, `Memory: ${s.memory_usage_bytes != null ? (s.memory_usage_bytes / 1048576).toFixed(1) + ' MB' : 'n/a'}`, `Network RX: ${s.network_rx_bytes ?? 'n/a'} bytes`, `Network TX: ${s.network_tx_bytes ?? 'n/a'} bytes`, `Uptime: ${s.uptime_seconds ?? 'n/a'} s`, '', '--- Raw metric families ---', JSON.stringify(r.families, null, 2), ].join('\n'); } else { pre.textContent = r.raw || '(no metrics — kata-monitor not running or sandbox not found)'; } } catch (err) { pre.textContent = `Metrics error: ${err.message || err}`; } }); }); panel.querySelector('#btn-kata-metrics-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-metrics-card'); if (card) card.style.display = 'none'; }); } // ── Utilities ─────────────────────────────────────────────────────── async function safe(p, fallback) { try { const v = await p; return v ?? fallback; } catch { return fallback; } } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderSkeleton() { return `
`; }