Skip to main content

Pods โ€” inventory, logs and exec

The Pods page lists every pod in the cluster and opens a macOS Terminal.app-style panel with a live log stream, an interactive shell (kubectl exec -it), the pod's events, or its YAML โ€” right in the browser. Pods can be restarted (when a controller owns them) or deleted, and logs can be popped out into their own browser tab. It is admin-only end to end: the nav link, the SPA routes, the REST APIs and both WebSockets all require the cluster.admin permission.

Pods list

Where to find itโ€‹

Entry pointNotes
Top nav Pods (between Virtual Machines and Compute)Hidden for user / viewer accounts
https://<HOST>:30152/app/podsNon-admins who open the URL directly see "Admins only."
Search โŒ˜K โ†’ "pods"Command palette
/app/pods/<ns>/<pod>/logsFull-window logs for one pod (the panel's โ†— new-tab target)

If the link is missing after an upgrade, hard-refresh the tab (โŒ˜โ‡งR) โ€” a tab opened before the deploy still runs the old bundle.

Pod listโ€‹

FeatureBehavior
Namespace filterAll namespaces (default) or any single namespace from GET /api/v1/namespaces
SearchMatches pod name, namespace and node
Status chipsAll ยท Running ยท Pending ยท Failed with live counts. Succeeded/Completed and Terminating are shown in the list but not counted as Pending
ColumnsName (+ owner kind, e.g. ReplicaSet, Job), Namespace, Status, Ready (ready/total main containers), Restarts (highlighted when > 0), Age, Node
Status textkubectl-style: a waiting/terminated reason on any main container (CrashLoopBackOff, ImagePullBackOff, Error, OOMKilled โ€ฆ) wins over the phase; a pod with a deletion timestamp shows Terminating
RefreshAutomatic every 10 s, plus the Refresh button in the header
ActionsLogs (any pod), Terminal (enabled only when the pod phase is Running), Restart โ†ป (enabled only for controller-owned pods) and Delete ๐Ÿ—‘

Clicking an action (or a row) opens the bottom panel:

  • Logs / Terminal / Events / YAML chips switch view for the same pod.
  • Header buttons: open logs in a new tab โ†—, restart, delete.
  • Drag the grip on the top edge to resize (default 460 px).
  • Maximize fills the viewport; Esc restores.
  • ร— closes the panel (the WebSocket closes with it).

Logs panelโ€‹

Pod logs

A read-only xterm.js terminal on a black Terminal.app "Pro" canvas (SF Mono 12.5 px, Terminal.app ANSI palette). Logs follow in real time.

Toolbarโ€‹

ControlWhat it does
LIVE badgeStream connected
Container selectShown for multi-container pods: main containers first, then init containers; defaults to the first main container
tail 100 / 500 / 2000Lines of history to load before following (server clamps to 1โ€“10 000)
timeAsk the kubelet to prefix each line with an RFC 3339 timestamp
previousLogs of the previous (crashed) container instance
Find (โ†‘ / โ†“)Incremental search through the scrollback
Pause / resumeFreeze the view; incoming lines are buffered and the count shown, then flushed on resume
ClearClear the screen (the stream keeps running)
DownloadSave the buffer as <namespace>_<pod>_<container>.log
ReconnectRe-open the stream

Changing container, tail, time or previous reconnects automatically. The browser keeps the most recent 20 000 lines.

Colorizingโ€‹

Lines are colored client-side (colorizeLogLine in web/src/components/terminalTheme.ts). Lines that already contain ANSI escapes are passed through untouched.

PatternColor
Leading RFC 3339 timestamp, klog time (I0927 01:25:45.123)dim grey
ERROR / ERR / FATAL / PANIC / CRIT (and klog E / F)bold red
WARN / WARNING (klog W)bold yellow
INFO / NOTICE (klog I)bold cyan
DEBUG / TRACEdim
level=โ€ฆ / lvl=โ€ฆ / severity=โ€ฆkey cyan, value by level
key= in logfmtcyan
"quoted strings"green
HTTP methods (GET, POST, โ€ฆ)bold blue
HTTP status codes (only on lines that look like HTTP)2xx green ยท 3xx cyan ยท 4xx yellow ยท 5xx bold red
JSON lines ({โ€ฆ} / [โ€ฆ])keys cyan ยท strings green ยท numbers magenta ยท true/false/null yellow ยท "level" values by level

Server notices arrive pre-colored: red error: โ€ฆ if the stream cannot start or breaks, grey [log stream ended] when the container exits.

Terminal (exec) panelโ€‹

Pod exec

An interactive TTY into the pod โ€” equivalent to kubectl exec -it <pod> -c <container> -- sh.

ControlWhat it does
Container selectSame list as Logs (shown for multi-container pods)
Shell auto / bash / shauto runs bash when present, otherwise sh
copy on selectSelecting text copies it to the clipboard (toggle)
ReconnectStart a new session
  • TERM=xterm-256color is exported, so top, vim, less, colors, and line editing work.
  • The terminal follows the panel size โ€” resizing/maximizing sends a resize to the TTY.
  • When the shell exits (exit, Ctrl-D) you see a grey [process exited N] line; press Reconnect for a new session.
  • Distroless images without /bin/sh cannot be exec'd into โ€” you'll get an error message from the API (use Logs instead).

Events and YAML panelsโ€‹

Pod events

Events โ€” equivalent to kubectl events -n <ns> --for pod/<pod>, refreshed every 5 s:

ColumnRendering
LAST SEENRelative age, grey
TYPENormal green ยท Warning yellow
REASONBold (orange for warnings), ร—N in magenta when the event repeated
SOURCEReporting component (kubelet, default-scheduler, โ€ฆ), blue
MESSAGEWrapped; yellow for warnings

Kubernetes only keeps events for about an hour, so an old healthy pod often shows none.

Pod YAML

YAML โ€” equivalent to kubectl get pod -n <ns> <pod> -o yaml, with managedFields stripped for readability. Line numbers in the gutter; keys cyan, strings green, numbers magenta, true/false/null yellow, comments and - markers grey (yamlLineSegments in terminalTheme.ts). copy puts the YAML on the clipboard, download saves <namespace>_<pod>.yaml, โ†ป reloads.

Restart and deleteโ€‹

ActionWhat happensEnabled when
RestartThe pod is deleted; its controller schedules a fresh replacement (new name, same spec). Same effect as kubectl delete pod on a Deployment podPod's controller owner is a ReplicaSet, StatefulSet, DaemonSet or ReplicationController
Deletekubectl delete pod with the default grace period. Controller-owned pods come back; bare pods are gone for goodAlways

Both ask for confirmation. The dialog says whether the pod will come back ("Its ReplicaSet will create a replacement") or not. Restart is refused server-side with 409 NOT_RESTARTABLE for anything else โ€” notably KubeVirt virt-launcher pods (owned by a VirtualMachineInstance: deleting one stops the VM rather than restarting it; use the VM's own Restart) and Job pods.

If the pod you delete is open in the panel, the panel closes.

Logs in a new tabโ€‹

The โ†— button in the panel header opens /app/pods/<namespace>/<pod>/logs?container=<c> in a new browser tab: the same log terminal filling the window, with a breadcrumb back to Pods. The URL is shareable between admins; if the pod no longer exists (for example after a restart) the page says so.

HTTP APIโ€‹

All routes need a bearer token (JWT or scoped API token) with cluster.admin.

GET /api/v1/podsโ€‹

QueryDefaultNotes
namespaceallOmit, empty, or all โ†’ every namespace; otherwise a DNS-1123 name
curl -sk -H "Authorization: Bearer $TOKEN" \
'https://HOST:30152/api/v1/pods?namespace=zorvia-system' | jq '.pods[0]'
{
"name": "zorvia-api-fb7cd456b-c8vvj",
"namespace": "zorvia-system",
"phase": "Running",
"status": "Running",
"ready": "1/1",
"restarts": 0,
"created": "2026-09-27T01:52:10Z",
"age_seconds": 1830,
"node": "nldw4-4-04-32",
"pod_ip": "10.42.0.117",
"owner_kind": "ReplicaSet",
"containers": [
{ "name": "zorvia", "image": "zorvia:local", "init": false, "ready": true, "restarts": 0, "state": "Running" }
]
}

Response envelope: { "pods": [...], "count": N }, sorted by namespace then name.

GET /api/v1/namespacesโ€‹

{ "namespaces": ["cdi", "default", "kubevirt", "zorvia-system"] }

DELETE /api/v1/pods/{namespace}/{pod}โ€‹

QueryDefaultNotes
gracepod's ownGrace period in seconds; 0 force-deletes
{ "deleted": true, "namespace": "default", "name": "web-7d9c-abcde" }

POST /api/v1/pods/{namespace}/{pod}/restartโ€‹

Deletes the pod only if a recreating controller owns it:

{ "restarted": true, "owner_kind": "ReplicaSet", "owner": "web-7d9c" }

GET /api/v1/pods/{namespace}/{pod}/eventsโ€‹

{
"events": [
{
"type": "Normal", "reason": "Pulled", "message": "Container image โ€ฆ already present",
"count": 1, "source": "kubelet",
"first_seen": "2026-09-27T05:12:01Z", "last_seen": "2026-09-27T05:12:01Z"
}
]
}

Sorted oldest โ†’ newest by last_seen (falls back to eventTime for new-style events).

GET /api/v1/pods/{namespace}/{pod}/yamlโ€‹

{ "yaml": "apiVersion: v1\nkind: Pod\nmetadata:\n name: โ€ฆ" }

GET /api/v1/pods/capabilitiesโ€‹

What the Zorvia ServiceAccount may do to pods cluster-wide, from three SelfSubjectAccessReviews (pods/log get, pods/exec create, pods delete):

{ "logs": true, "exec": true, "delete": false }

The page uses this to disable actions whose RBAC rule is missing โ€” typically after upgrading Zorvia without re-applying the ClusterRole โ€” and shows a yellow notice naming the missing rules. If the review itself fails, the capability reports true; the Kubernetes call still enforces RBAC.

Errorsโ€‹

StatusCodeWhen
400INVALIDNamespace / pod is not a valid Kubernetes name
401โ€”Missing / invalid / revoked token
403โ€”Authenticated but not cluster.admin
403FORBIDDENThe ServiceAccount lacks the Kubernetes permission (e.g. pods delete)
404NOT_FOUNDPod does not exist
409NOT_RESTARTABLERestart on a pod without a recreating controller
500LIST_FAILED / KUBE_ERROROther Kubernetes API error (message sanitized)

WebSocket protocolโ€‹

Browsers cannot set Authorization on a WebSocket upgrade, so the token travels as ?token=. Both sockets validate the token and the cluster.admin permission before upgrading (HTTP 401 / 403 otherwise), and reject non-DNS-1123 namespace, pod or container names with 400.

Logs โ€” GET /ws/pods/{namespace}/{pod}/logsโ€‹

wss://<HOST>:30152/ws/pods/<ns>/<pod>/logs?token=<jwt>&container=<c>&tail=500&timestamps=1&previous=0
QueryDefaultNotes
tokenโ€”Required
containerpod defaultRequired by Kubernetes for multi-container pods
tail500Clamped to 1โ€“10 000
timestampsoff1 / true / yes
previousoff1 / true / yes

Server โ†’ client: one text frame per log line (always follow=true), plus a WebSocket ping every 25 s to keep proxies from idling the connection out. Client โ†’ server: nothing (close to stop).

Exec โ€” GET /ws/pods/{namespace}/{pod}/execโ€‹

wss://<HOST>:30152/ws/pods/<ns>/<pod>/exec?token=<jwt>&container=<c>&shell=auto
QueryDefaultNotes
tokenโ€”Required
containerpod default
shellautoauto, bash or sh โ€” anything else behaves like auto

The command is never user-supplied: the server always runs /bin/sh -c "export TERM=xterm-256color; exec <bash|sh>".

DirectionFramePayload
client โ†’ serverbinaryRaw keystrokes โ†’ TTY stdin
client โ†’ servertext{"type":"resize","cols":120,"rows":40} (clamped 2โ€“1000)
server โ†’ clientbinaryRaw TTY output (stdout + stderr)
server โ†’ clienttext{"type":"exit","code":0} when the shell exits (code may be null)
server โ†’ clienttext{"type":"error","message":"โ€ฆ"} if exec could not start

Minimal client:

const ws = new WebSocket(`wss://${host}:30152/ws/pods/default/web-0/exec?token=${jwt}`)
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) => typeof e.data === 'string'
? console.log('control', JSON.parse(e.data))
: term.write(new Uint8Array(e.data))
term.onData((d) => ws.send(new TextEncoder().encode(d)))
term.onResize(({ cols, rows }) => ws.send(JSON.stringify({ type: 'resize', cols, rows })))

Security modelโ€‹

Exec into arbitrary pods โ€” including kube-system and Zorvia itself โ€” is a cluster-admin-equivalent capability, so the feature is locked down accordingly:

  1. REST RBAC โ€” required_permission in src/api/auth/permissions.rs maps every /v1/pods* and /v1/namespaces* request (any method, including GET) to ApiPermission::ClusterAdmin, ahead of the "reads are allowed" fallback. Unit test: pods_and_namespaces_require_cluster_admin.
  2. WebSocket RBAC โ€” /ws/* routes sit outside the /api middleware, so the pod sockets call authorize_permission(state, token, ApiPermission::ClusterAdmin) themselves (401 missing/invalid token, 403 insufficient permission). Session revocation (token_version) applies because tokens are resolved through the same resolve_credential path as REST.
  3. Input validation โ€” namespace, pod and container names must be DNS-1123 (lowercase alphanumerics, -, .; โ‰ค 253 chars), so nothing path-like reaches the Kubernetes URL.
  4. Fixed argv โ€” the exec command is chosen from three hard-coded launchers.
  5. Restart guard โ€” restart only deletes pods whose controller recreates them; virt-launcher (VM) and Job pods are refused with 409.
  6. UI gating โ€” nav link and routes are admin-only, destructive actions confirm first (UX only; the server is the enforcement point).
  7. Audit โ€” every log view, exec start/end, restart and delete is recorded (below).

Tokens in the WebSocket query string can appear in reverse-proxy access logs; prefer short-lived JWTs and scrub token= in any proxy in front of Zorvia.

Kubernetes RBACโ€‹

The zorvia ServiceAccount needs, in addition to its existing pods / events / namespaces get-list access:

- apiGroups: [""]
resources: ["pods"]
verbs: ["delete"] # Restart + Delete
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "get"] # get: WebSocket upgrade; create: SPDY/v5 exec

These rules ship in deploy/k8s.yaml, deploy/k3s-zorvia-web.yaml and the Helm chart (charts/zorvia/templates/rbac.yaml, both cluster-scoped and namespaced modes). In namespaced mode the all-namespaces list needs cluster-scoped pods list; otherwise pick a namespace the chart grants.

Existing installs โ€” re-apply the manifest / helm upgrade, or patch the live role:

kubectl patch clusterrole zorvia --type=json -p='[
{"op":"add","path":"/rules/-","value":{"apiGroups":[""],"resources":["pods"],"verbs":["delete"]}},
{"op":"add","path":"/rules/-","value":{"apiGroups":[""],"resources":["pods/log"],"verbs":["get"]}},
{"op":"add","path":"/rules/-","value":{"apiGroups":[""],"resources":["pods/exec"],"verbs":["create","get"]}}
]'

SA=system:serviceaccount:zorvia-system:zorvia
kubectl auth can-i delete pods --as=$SA -A # yes
kubectl auth can-i list events --as=$SA -A # yes
kubectl auth can-i get pods --subresource=log --as=$SA -A # yes
kubectl auth can-i create pods --subresource=exec --as=$SA -A # yes

(kubectl auth can-i create pods/exec reports no even when granted โ€” use --subresource.)

Without pods delete, Restart/Delete return 403 FORBIDDEN; everything else keeps working. If you don't want pod deletion from the console at all, omit that rule.

Audit trailโ€‹

EventactionSeverityresource_namedetails
Log stream openedview_logsInfo<pod>/<container>โ€”
Exec startedexecHigh<pod>/<container>{"shell":"auto","event":"start"}
Exec endedexecHigh<pod>/<container>{"shell":"auto","event":"end","exit_code":0,"duration_secs":42}
Exec failed to startexec (success=false)High<pod>/<container>{"shell":"auto","error":"โ€ฆ"}
Pod restartedrestartInfo<pod>{"owner_kind":"ReplicaSet","owner":"web-7d9c"}
Pod deleteddeleteInfo<pod>{"grace":null} (success=false when Kubernetes refused)

resource_type is pod, namespace is the pod namespace, user is the signed-in username (api-token for token principals). Export for a SIEM:

curl -sk -H "Authorization: Bearer $TOKEN" \
'https://HOST:30152/api/audit/export?format=jsonl&limit=200' | jq 'select(.action=="exec")'

Troubleshootingโ€‹

SymptomCause / fix
No Pods linkNot an admin, or stale tab โ€” hard-refresh
"Could not load pods" 403Token lacks cluster.admin
"Could not load pods" 500 forbiddenServiceAccount missing pods list at cluster scope
Logs: error: cannot stream logs โ€ฆ forbiddenMissing pods/log get โ€” see Kubernetes RBAC
Logs: a container name must be specifiedPick a container in the select
Logs: previous terminated container โ€ฆ not foundTurn previous off (container never restarted)
Terminal: error โ€ฆ forbiddenMissing pods/exec create/get
Terminal: executable file not foundImage has no /bin/sh (distroless)
Terminal button disabledPod is not Running
Restart button disabledNo recreating controller (bare pod, Job, or KubeVirt virt-launcher) โ€” use Delete, or the VM's Restart
Yellow "โ€ฆ unavailable" notice; Logs / Terminal / Restart / Delete greyed outServiceAccount missing the named rule (pods/log, pods/exec, pods delete) โ€” re-apply the ClusterRole (UPGRADE.md); hover a button for the exact rule
Restart / Delete: 403 FORBIDDENServiceAccount missing pods delete
Events panel emptyEvents expired (โ‰ˆ1 h retention) โ€” normal for long-running healthy pods
New-tab logs: "Pod โ€ฆ was not found"The pod was replaced (e.g. after Restart) โ€” open the new pod from the list
Panel connects then drops after ~60 s behind a proxyProxy idle timeout; logs send pings every 25 s โ€” raise the proxy timeout for exec sessions

Testingโ€‹

  • Rust: cargo test --features web --lib -- pod_handlers permissions::tests (name validation, fixed shell argv, resize control parsing, restartable owners, ClusterAdmin gate for every pods route and method).
  • Vitest: web/src/components/terminalTheme.test.ts โ€” log colorizer (levels, klog, logfmt, JSON, HTTP method/status, quoted request lines, ANSI passthrough, visible text unchanged) and the YAML tokenizer.
  • Lab e2e (web/e2e/lab-pages.spec.ts):
    • top nav links to pods โ€” top-nav click lands on /app/pods with rows.
    • pods: list, logs and exec โ€” selects zorvia-system, opens Logs on a Running zorvia-api pod and asserts streamed lines, then opens Terminal and runs echo zorvia-$((40+2))-ok, expecting zorvia-42-ok (proves real shell evaluation).
    • pods: events, yaml, logs tab, delete confirm โ€” Events shows the kubectl events prompt, YAML contains kind: Pod without managedFields, โ†— opens a new tab on /app/pods/<ns>/<pod>/logs with streamed lines, Delete shows the "ReplicaSet will create a replacement" confirmation (cancelled โ€” the lab API pod is never deleted).
cd web
ZORVIA_E2E_PASSWORD=โ€ฆ ZORVIA_E2E_BASE_URL=https://HOST:30152 PLAYWRIGHT_BASE_URL=https://HOST:30152 \
npx playwright test e2e/lab-pages.spec.ts --project=chromium -g "pods"

Implementation mapโ€‹

LayerFile
REST + WebSocket handlerssrc/api/http_server/web/pod_handlers.rs
Route registrationsrc/api/http_server.rs (/v1/pods[/{ns}/{name}[/restart|events|yaml]], /v1/namespaces, /ws/pods/{ns}/{name}/logs|exec)
Permission gatesrc/api/auth/permissions.rs, authorize_permission in ws_proxy_handlers.rs
Audit actionssrc/audit_trail/mod.rs (Exec, ViewLogs, Restart, Delete)
API clientweb/src/api/pods.ts
Pagesweb/src/pages/Pods.tsx, web/src/pages/PodLogsPage.tsx (new-tab logs)
Panelsweb/src/components/PodLogs.tsx, PodExec.tsx, PodEvents.tsx, PodYaml.tsx
Terminal.app theme, log colorizer, YAML tokenizerweb/src/components/terminalTheme.ts (+ terminalTheme.test.ts)
Styles.zf-terminal-pro, .zf-term-* in web/src/styles/main.css