Skip to content

feat: show Docker and Podman together, plus KVM/libvirt domains - #2

Merged
nixfred merged 2 commits into
nixfred:mainfrom
kanthi:feat/podman-container-metrics
Sep 18, 2026
Merged

nixfred merged 2 commits into
nixfred:mainfrom
kanthi:feat/podman-container-metrics

Conversation

@kanthi

@kanthi kanthi commented Sep 9, 2026

Copy link
Copy Markdown

Why

The collector already shows Docker containers. Hosts that only run Podman (typical on Omarchy: Docker client present, no dockerd, rootless Podman socket) produced empty container sections, even when podman ps listed running workloads.

This is based on current main (33c8e17, including Fileri's bounded-read / PlainText hardening). Docker-first behaviour is unchanged.

Core collector (backend/collect.ts)

This is the agentless SSH script plus the parsers that turn its stdout into the snapshot the panel renders. Nothing is installed on the remote.

1. Runtime probe: Docker first, then Podman

Before:

if sudo -n docker version; then DOCKER="sudo -n docker"
elif docker version; then DOCKER="docker"
else DOCKER=""
fi
$DOCKER ps / stats / inspect

docker version fails closed when /var/run/docker.sock is missing (Omarchy ships the Docker client without a daemon). The script then emitted empty @@DOCKER_PS@@ / STATS / INSPECT blocks.

After, the same four-way probe, still Docker-preferring:

  1. sudo -n docker version — operator accounts outside the docker group
  2. docker version — docker-group hosts
  3. sudo -n podman version — rootful Podman
  4. podman version — rootless Podman for the SSH user

The selected binary is stored in CTR and used for ps --all, stats --no-stream, and the existing narrow inspect --format (no env vars). Section markers stay @@DOCKER_*@@ so splitSections, the snapshot cache, and MAX_CONTAINERS cap do not change.

readBounded / SSH byte budgets from the security work are untouched.

2. Why a parser change is required (not just swapping the binary)

Podman --format "{{json .}}" is not Docker's JSON:

Field Docker Podman
ps Names string "web-1" array ["openclaw"]
ps Status "Up 6 days (healthy)" often "" (use State)
stats CPU CPUPerc: "9.39%" CPU: 1.95 (number)
stats memory MemUsage: "1.254GiB / 4GiB" MemUsage + MemLimit as bytes
stats MemPerc / PIDs strings numbers
inspect Name "/web-1" "openclaw" (no slash; already stripped)

If we only switched the CLI, names would mostly work (String(["openclaw"]) happens to stringify), but CPU/memory cards would stay empty because CPUPerc and "a / b" MemUsage are missing.

parseContainers now:

  • psName() — first element if Names is an array
  • cpuPercentFromStats() — CPUPerc (strip %) or numeric CPU
  • memFromStats() — "used / limit" via parseSize, or numeric MemUsage/MemLimit/MemPerc
  • status falls back to State when Podman leaves Status blank
  • PIDs accepts string or number

Docker sample fixtures still parse the same (existing parseContainers test). ps.slice(0, MAX_CONTAINERS) is unchanged.

3. Host net counters

podman0 is ignored the same way as docker0, so the bridge is not counted as physical traffic.

Tests

  • Probe order: script still contains sudo -n docker version and the podman fallback.
  • Fixture from live Podman JSON (Names: ["openclaw"], numeric CPU/mem) joins ps/stats/inspect.
  • Existing Docker fixtures, disk grouping, and bounded-read tests still pass (bun run check: 50 tests).

Docs / copy

README, server-status.ts usage, and the Panel observer-effect comment now say Docker or Podman. Requirements: sudo -n docker / docker group or a working podman for the SSH account (rootless is enough).

Out of scope

  • No new remote agent, socket proxy, or DOCKER_HOST rewrite.
  • No AUR/Incus.
  • Rootless Podman is only visible if SSH lands as the user that owns the containers (same account constraint as docker-group today).

Verified against a live Omarchy host over Tailscale SSH: five rootless Podman containers (openclaw, hermes, beszel, ntfy, beszel-agent) appear with CPU/mem.

@kanthi

kanthi commented Sep 9, 2026

Copy link
Copy Markdown
Author

Follow-up on this PR:

  • Docker and Podman at once. The collector no longer stops at the first working engine. Both ps/stats/inspect passes run. If docker info says Podman, the docker pass is dropped so the same containers are not listed twice.
  • KVM/libvirt VMs. Read-only virsh list + dominfo on qemu:///system and qemu:///session (sudo -n virsh when that is what works). They show as cards with runtime: kvm (memory from balloon; CPU% is not sampled).
  • Panel header becomes CONTAINERS · n · VMS · m when both kinds are present. Detail line includes docker / podman / kvm.

Still one read-only SSH round trip. 52 tests pass.

@nixfred

nixfred commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Thanks for this — and for the follow-up commit. Reviewed against current main (75fd1f9, which adds theme-derived status colours since you branched). It merges cleanly and the full suite passes at 64 tests.

The short version: the Podman work is good and I would like to take it. Could you split the KVM half into its own PR?

Podman: yes

The parser work is the part that makes this real, and it is correct. Swapping the binary alone would have left CPU and memory cards empty, because Podman gives Names as an array, CPU as a number rather than a percent string, and memory as separate byte fields. Handling all three, keeping the probe Docker-first, reusing the @@DOCKER_*@@ markers so splitSections and the snapshot cache are untouched, and applying MAX_CONTAINERS after merging the engines rather than per-engine — all right. The podman0 net-counter exclusion is a nice catch.

One regression to fix before it lands

Changing container warning ids from container-<name> to container-<runtime>-<name> is the correct call for disambiguation, but every mute a user has already recorded names the old id, so all of them silently revert and the warnings someone deliberately silenced come back.

Not hypothetical: the maintainer's live selection file holds 13 container mutes, ten of them a kasm_* stack on one host. All 13 would have been orphaned.

I have a fix on review/pr-2-mute-compat (commit cdc419d) if it is useful — isWarningMuted() also accepts the legacy id for docker containers, and toggleWarningMute() clears the legacy entry too so unmuting does not immediately re-mute. Verified against that real config: 13 of 13 preserved. Take it, rewrite it, or tell me you would rather handle it your way.

KVM: separate PR please

Two reasons, neither of them "no".

It is a different feature. VMs modelled as ContainerMetrics with runtime: "kvm" is a real design decision, and it deserves its own discussion rather than arriving inside a Podman PR.

It costs more than it looks. Both qemu:///system and qemu:///session are queried, and dominfo runs once per domain per URI. I measured it with instrumented shims rather than eyeballing it: a host with Podman and six domains runs 21 engine and VM processes per refresh, where main runs about five, and the cap allows up to 128 dominfo spawns.

That matters here specifically because of your own earlier find. The observer effect you diagnosed in #1 is exactly this — the monitor's own connection spiking the remote run queue. The load5 change absorbs it, but this makes the burst it has to absorb about four times larger.

A batched call would mostly remove the concern. virsh domstats --state --balloon --vcpu reports every domain in one invocation, so six domains would cost two or three spawns instead of sixteen. I could not verify that locally — libvirt is not installed on this machine — so please sanity-check the flags on your host before committing to it. Querying qemu:///system and only falling back to qemu:///session when it yields nothing would halve it again, and would also fix a smaller thing I noticed: parseKvm dedups on uri::name, so a domain visible under both URIs is listed twice rather than once.

Minor

manifest.json and pluginVersion are not bumped; there is a test that asserts those two agree, so whichever value you pick just needs to be in both.

Nothing here is merged or closed yet — that is the maintainer's call once the split lands.

@kanthi

kanthi commented Sep 11, 2026

Copy link
Copy Markdown
Author

Understood. I will consider KVM into a separate PR . I am using agents to most of the work here . Will update as per your suggestions

nixfred added a commit that referenced this pull request Sep 14, 2026
WHAT: bumps manifest.json, package.json and pluginVersion to 0.9.0 for the
Podman support landed from PR #2 (kanthi's commit a63bfbe, cherry-picked
with authorship intact as 77f56c3).

WHY: on a host that runs only rootless Podman, the collector probed Docker,
found no daemon, and emitted empty container sections, so the panel showed
no containers at all. Omarchy ships the Docker client without dockerd, which
makes that the common case rather than an edge.

SCOPE: Podman-only, as agreed with kanthi on PR #2. The probe is still one
first-working engine (sudo docker, docker, sudo podman, podman). The
parser handles Podman's differing JSON: Names as an array, CPU as a number,
memory as separate byte fields, blank Status. podman0 is excluded from host
net counters. Container warning ids are unchanged, so every existing mute
still applies. The KVM/libvirt commit (dc13784) stays out pending its own
PR, because it adds two virsh spawns per domain per refresh.

MEASURED: instrumented shims, rootless-Podman host with six libvirt domains,
per refresh: main 1 spawn and no containers shown, this release 6 spawns
with containers shown, the full PR 22 spawns of which 16 are virsh.

CHECKS: 63 tests pass, 27 of 27 Text elements pinned to PlainText, qmllint
clean, omarchy plugin validate clean.

LR-T: omarchy-server-status, podman, containers, release, v0.9.0
LR-D: desktop, omarchy, containers
LR-K: podman-rootless-collection, pr2-podman-only-split
LR-P: release

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nixfred

nixfred commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Thank you, kanthi — the Podman support is merged and released.

Your commit landed on main as 77f56c3 with your authorship intact, and shipped in v0.9.0 (64c7d4b, tagged). This is the split we talked about: Podman on its own, KVM to follow separately. I cherry-picked it rather than waiting because it closes a real gap. On a host running only rootless Podman, main found no Docker daemon and showed no containers at all.

Measured before merging, with instrumented shims on a rootless-Podman host running six libvirt domains, per refresh:

  • main: 1 spawn, no containers shown
  • Podman-only (now released): 6 spawns, containers shown with CPU and memory
  • this full PR: 22 spawns, 16 of them virsh

It passed 63 tests on current main, all 27 Text elements are still pinned to PlainText, and qmllint and omarchy plugin validate came back clean. It also changes no container warning ids, so every existing mute keeps working.

This PR can become the KVM PR. Since a63bfbe is now on main, rebasing feat/podman-container-metrics onto main should leave only the KVM work. dc13784 builds on the Podman commit, so expect a conflict or two there. When you pick it back up, three things would make it straightforward to take:

  1. Batch the domain queries. virsh domstats --state --balloon --vcpu reports every domain in one call. I still have not been able to verify the flags here, so please check them on your host.
  2. Query qemu:///system first and fall back to qemu:///session only when it yields nothing. That halves the cost and fixes a domain being listed twice under both URIs.
  3. The container-<runtime>-<name> warning id change needs the legacy-id compatibility path, or existing mutes silently revert. There is a ready fix on review/pr-2-mute-compat you are welcome to take.

No rush on any of it. Thanks again — between the observer-effect diagnosis in #1 and this, you have made the plugin meaningfully better.

- Collect Docker and Podman independently so hosts running both show both
- Query libvirt domains in a single batched virsh domstats call
- Query qemu:///system first and fall back to qemu:///session only when empty
- Deduplicate domains by name across URIs
- Preserve backwards compatibility for container warning mutes
- Bump plugin and manifest version to 0.10.0
@kanthi
kanthi force-pushed the feat/podman-container-metrics branch from dc13784 to bb3e5bd Compare September 16, 2026 13:33
@kanthi kanthi changed the title Collect Podman containers when Docker is not available feat: show Docker and Podman together, plus KVM/libvirt domains Sep 16, 2026
@kanthi

kanthi commented Sep 16, 2026

Copy link
Copy Markdown
Author

Updated as suggested:

  1. Batched domain queries: Switched from per-domain virsh dominfo loops to a single virsh domstats --state --balloon --vcpu invocation, minimizing process spawning and preventing run-queue spikes.
  2. URI fallback & Deduplication: Probes qemu:///system first and only falls back to qemu:///session if the system URI yields nothing. In addition, parseKvm deduplicates domains by name across URIs.
  3. Mute ID backwards compatibility: Integrated the compatibility logic from review/pr-2-mute-compat (legacyWarningId and updated toggleWarningMute) so existing container-<name> user mutes are preserved and unmuting clears legacy IDs cleanly.
  4. Rebase & version bump: Cleanly rebased onto main (v0.9.1). Bumped version to 0.10.0 across manifest.json, package.json, and Panel.qml.
  5. Passes all 72 tests in the suite and omarchy plugin validate.

…ions

- Filter containers and KVM domains into appContainers and virtualMachines
- Display distinct CONTAINERS and VIRTUAL MACHINES section headers with item counts
- Add VmTile component with [KVM] badge, vmDetail formatter, and alert mute toggle
- Ensure VM name text in VmTile elides properly with Layout.fillWidth
- Add unit test for section separation; passes all 73 tests
@kanthi

kanthi commented Sep 16, 2026 •

Copy link
Copy Markdown
Author

Added a follow-up commit to visually separate Containers and Virtual Machines in the panel UI:

  1. Dedicated UI Sections: Containers and KVM domains are now filtered into distinct CONTAINERS · X and VIRTUAL MACHINES · Y sections instead of sharing a generic container list.
  2. Dedicated VmTile Component:
    • Displays a compact [KVM] badge beside the domain name.
    • Includes tailored vmDetail formatting showing memory allocation/usage and domain state.
    • Text layout is constrained with Layout.fillWidth: true and proper eliding so right-aligned badges (memory percentage, MUTE / MUTED toggles) are never pushed off-screen.
  3. Muting & Compatibility: Full mute toggle support is wired up for VM warnings and maintains backwards-compatibility with existing mutes.
  4. Testing:
    • Added unit test for section separation.
    • All 73 tests pass (bun test) and omarchy plugin validate . is clean.

@nixfred

nixfred commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Thank you, and thank you for basing it on current main including Fileri's bounded-read hardening. Podman-only hosts showing empty container sections is exactly the kind of gap worth closing, and separating VMs from containers reads better than mixing them.

I audited the elevated paths before merging, since a bar plugin reaching for sudo deserves it. Every command that can run under sudo -n is read-only: docker/podman info, ps --all, ps -aq, inspect --format, stats --no-stream, and virsh domstats/dominfo. Nothing that starts, stops, removes or defines anything. The sudo -n count is unchanged from main, so this adds no new escalation, only new read paths. Checked out the branch and ran the suite: 73 pass, 0 fail.

Merging.

@nixfred
nixfred merged commit 0abe109 into nixfred:main Sep 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants