- Go 99.8%
- Makefile 0.1%
- Shell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo | ||
| cmd | ||
| docs | ||
| internal | ||
| .gitignore | ||
| .tagme-authors.json | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| cliff.toml | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| Makefile | ||
| README.md | ||
| SECURITY-REVIEW.md | ||
| SECURITY.md | ||
pods-ctl
A Go CLI and TUI for managing Podman pods as systemd services, supporting both rootless (user session) and rootful (system-level) Podman.
Description
pods-ctl talks to systemd over D-Bus and to Podman over its REST API, so you can list, start, stop, restart, inspect, pull, upgrade, prune, and browse pods without needing root privileges. It keeps a local SQLite registry of your quadlet .pod/.container definitions, resolves pod-service-container mappings, detects image and healthcheck drift, and exposes everything through both a traditional command set and an interactive terminal UI. Image drift detection and image pulling work even when pods are stopped, reading declared images directly from quadlet .container files.
Core value proposition: reliably manage Podman pods as systemd services with clear status, health checks, declared-vs-running configuration drift detection, and lifecycle control from a single self-contained binary. Both rootless (user session) and rootful (system-level) Podman are supported.
Features
CLI commands
browse— launch the interactive TUIstart [target...],stop [target...],restart [target...]— lifecycle controlstatus [target...](aliasps) — show pod status and per-container healthcheck configuration, exits non-zero on failureslist(aliasls) — registry view with live container count and statusdiscover— scan the configured base directory and populate the registrycheck— cross-check registry against on-disk.podfiles under the configured base directorycleanup— remove stale registry entriesvalidate [target...]— validate registry entriesvalidate-config— validate configuration, registry, and runtime connectivityhistory [target...]— who changed what and when: every state-changing CLI and TUI action with actor, target and result (--limit,--since,--action,--source,--failed); dry runs are not recordeddiff [target...]— show declared-vs-running image and healthcheck differences without changing anything; exits 1 when anything differsexec <pod[:container]> [-- command...]— run a command or an interactive shell inside a container as the pod's user; returns the command's exit statusdoctor— diagnose registry, base directory, quadlet drift and per-user setup (account, lingering, systemd session, Podman socket) with a fix hint for each failurehealth [target...]— deep container health check with declared vs running healthcheck config and drift detection, restart diagnostics (crash-loop detection, OOM kills, why the last run ended); reports unhealthy when expected containers are missing from Podmanreload [target...]— runsystemctl --user daemon-reloadrecover [target...]— start stopped/exited/unhealthy containers and start missing containers not found in Podman but declared in the registry cacheprune [target...]— remove images (and with--volumes, volumes;--allis kept as an alias) that no registered quadlet declares, plus any exited container, per pod user;--broadruns the previouspodman system prune -aDefault changed:pods-ctl pruneno longer runspodman system pruneby default — it removes only resources no registered quadlet declares, using the same fail-closed computation and pre-delete re-scan as the TUI'sCtrl+x, plus any container currently in the "exited" state (no quadlet-reference check needed, since a stopped container is always safe to remove). Flag names are unchanged; pass--broadfor the previous behavior.update [target...]—podman auto-updateper pod userpull [target...]— pull all declared container images for pod user(s); works with stopped pods by readingImage=from quadlet.containerfiles via systemdupgrade [target...]— restart pods where the running image or healthcheck config differs from the quadletrefresh-deps [target...]— cache systemd dependency graph and image drift in DB; works with stopped podsremove [target...](aliasrm) — remove pods from the registry and clean up dependenciesimages [target...]— list collected container images and versionsstats [target...]— live CPU/memory/PID/network usage per containerevents— stream Podman lifecycle events (--since,--until,--type,--name)logs <target>— stream container/pod logs (--tail,--follow,-t,--since,--until)export --output <path>— export registry data to JSON (-o -for stdout)import --input <path>— import registry data from JSON (-i -for stdin; merge or replace)completion [bash|zsh|fish|powershell]— generate shell completion scriptsman <dir>— generate Unix man pages
Global flags: --no-color, --json, --dry-run, --quiet/-q, --plain, --no-input, --verbose/-v, --log-format, --timeout, --user, --base-dir, -f/--file, --config. --compact (health, stats) and --watch (health, status, stats) are flags of those commands only. Run pods-ctl --help for commands grouped by purpose (lifecycle, inspect, registry, maintenance).
Destructive commands (stop, restart, prune, upgrade, remove) accept --force to skip confirmation prompts.
Exit codes: 0 success · 1 the command ran and failed (any failed target, unhealthy/failed pods for status/health, differences for diff, failed checks for doctor/validate, or the wrapped command's own status for exec) · 2 invalid invocation (unknown command or flag, bad arguments, invalid --user) · 70 internal error (panic). With --json, every top-level object carries "schema_version" (currently 1; bumped only when an existing field changes meaning or is removed).
Shell completion: pods-ctl completion bash|zsh|fish|powershell prints the script; pod names (with their user and description), --user values and --log-format values complete dynamically from the registry — no root needed. Example: pods-ctl completion bash | sudo tee /etc/bash_completion.d/pods-ctl.
TUI features (pods-ctl browse)
- Tree view: users → pods → containers with live status, health, CPU%, memory, restart counts, and sparkline CPU history
- Tabbed right-hand stack: Pod summary, Container summary, Details/Inspect, and Diff panes
- Details pane with an interactive, collapsible JSON tree of the full
podman inspectoutput for the selected pod or container - Diff pane: compare running vs quadlet-declared images for a pod or container
- Logs pane: live container/pod log stream with bounded scrollback, follow/pause toggle, and
/search filtering - Events pane: live Podman lifecycle event stream that resumes from the last seen time and reconnects automatically with backoff when the stream drops
- Health overlay (
h): per-container status/health/restarts/exit-code table with recent failure messages, healthcheck configuration indicators, and drift markers when declared and running configs differ - Container details pane: dedicated Healthcheck section showing the declared quadlet config, the running
podman inspectconfig, and any drift fields - Bulk action palette (
Ctrl+a/.) with grouped actions, select-all/invert, and clear selection, plus live per-row spinner badges and a footer progress counter while a bulk batch is running - Action timeline overlay (
Ctrl+t): session log of every lifecycle action taken, with success/failure markers - Network topology overlay (
Ctrl+g): pod/container tree annotated with IP addresses - Global log search overlay (
Ctrl+/): search recent log lines across all expanded pods at once - Pod creation wizard (
Ctrl+w): 4-step guided form that previews generated quadlet.pod/.containerfile text, with pod-name, port-format, and registry-collision checks - Resource usage/stats overlay (
7): live CPU/memory/PID/network/block-I/O metrics with gauge bars and sparkline history for a selected pod or container, plus pod/user context and a?shortcut into the quadlet resource-limit help - Port mappings overlay (
P): published container ports and their host bindings for the selected pod or container - Volumes overlay (
V): mounts for the selected pod/container, each marked declared, runtime only, or unknown against the pod's quadletVolume=references, with driver/mountpoint for named volumes - Networks overlay (
W): the selected pod's quadlet-declaredNetwork=references resolved against live Podman network metadata, with an explicit note when a selected container shares the pod's network namespace - Images overlay (
I): the image each container in the selection runs, with size and in-use container count from a single Podman image list call - Process list overlay (
t): one-shot column-aligned process list for the selected container, fetched from Podman'stopoutput with manual refresh (r) rather than a second polling loop - Disk usage overlay (
D): global host-wide disk usage from any focus, split into quadlet-managed vs. host-only images/containers/volumes that always sum to the host total, with a header showing hostname, OS/kernel, memory/swap usage, and Podman/Buildah/OCI-runtime/conmon versions - Registry check overlay (
C): global, read-only merged view ofpods-ctl check's stale/unregistered drift andpods-ctl validate's per-entry errors/warnings in one scrollable list with a summary line, loaded on open and refreshed only onr - Quadlet-scoped prune (
Ctrl+x): removes volumes and images that no registered quadlet declares, with a scrollable candidate list, a default-No confirmation, and a re-verified candidate set - Recover (
Ctrl+r/ palette "Recover"/"Recover selected"): classifies the cursor container or a multi-selection via the same recovery logic the CLI'srecovercommand uses, starting stopped/exited containers and restarting unhealthy ones while leaving healthy containers untouched — single-target runs immediately, a selection confirms first (default No); a selected pod recovers its loaded container children only - Filtering with quick filter keys (
f1–f5) and saved filter presets, sort toggles, pinned/favorite pods, undo stack for container actions, and toast notifications - Full mouse support: click to focus panes, scroll wheel, double-click to open logs, right-click context menu, and clickable overlays
- Keyboard-driven interface with context-aware help overlay
Installation
Build the self-contained binary from source (requires Go 1.27+):
make build
The Makefile stamps the version, commit, and build date from git using -trimpath and -ldflags "-s -w -X ...cli.Version=... -X ...cli.Commit=... -X ...cli.BuildDate=...". To build manually:
go build -trimpath -ldflags "-s -w \
-X git.desord.re/desordre/pods-ctl/internal/cli.Version=$(git describe --tags --always) \
-X git.desord.re/desordre/pods-ctl/internal/cli.Commit=$(git rev-parse --short HEAD) \
-X git.desord.re/desordre/pods-ctl/internal/cli.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o pods-ctl .
Install to /usr/local/bin (default prefix):
sudo make install PREFIX=/usr/local
The resulting binary is pods-ctl.
Quick start
# Scan quadlet definitions under the configured base directory and populate the registry
pods-ctl discover
# Show all registered pods with live status
pods-ctl list
# Check overall pod health and healthcheck config drift
pods-ctl health
# Start, stop, or restart pods (target is quadlet pod name, optionally with :container)
pods-ctl start myapp
pods-ctl stop myapp:web
pods-ctl restart myapp
# Pull declared images (works even if the pod is not started)
pods-ctl pull myapp
pods-ctl pull # all pods
# Upgrade pods where the running image or healthcheck differs from the quadlet
pods-ctl upgrade
# Watch live status
pods-ctl status --watch
# Launch the interactive TUI
pods-ctl browse
# Stream Podman events
pods-ctl events
# Scripting: plain text output (tab-separated, one record per line)
pods-ctl list --plain
# Scripting: suppress non-essential output
pods-ctl start myapp --quiet
# Scripting: JSON output piped to jq
pods-ctl status --json | jq '.pods[] | select(.status=="failed")'
# Force destructive action without confirmation (for scripts/CI)
pods-ctl stop --force
pods-ctl prune --force
# Run the previous broad "podman system prune" behavior
pods-ctl prune --broad --force
System requirements
- Linux with systemd and D-Bus
- Rootless Podman with the user socket available at
/run/user/{uid}/podman/podman.sock, or rootful Podman with the system socket at/run/podman/podman.sock - Go 1.27+ for building from source
- A terminal with TrueColor support recommended for the TUI
Configuration
pods-ctl loads configuration from:
- A config file passed with
--config /etc/pods-ctl/config.*$HOME/.config/pods-ctl/config.*- Environment variables prefixed with
PODS_CTL_(for example,PODS_CTL_REGISTRY)
Supported configuration keys: registry, base_dir, no_color, json, dry_run, quiet, plain, no_input, verbose, log_format, timeout, user. The default registry database is /etc/pods/registry.db and the default base directory for quadlet definitions is /srv/storage.
Environment variables: PODS_CTL_* prefix for all config keys (e.g. PODS_CTL_REGISTRY), plus NO_COLOR and DEBUG for color and verbosity control, and standard proxy vars (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY). NO_COLOR also defaults the browse TUI to no color, overridable by a config file's no_color, PODS_CTL_NO_COLOR, or --no-color.
See docs/configuration/ for detailed configuration guidance.
Architecture overview
main.go
│
▼
cmd/ ← Cobra command tree and shared helpers
┌─────────┐
│ browse │ ────────────┐
│ start │ │
│ stop │ ▼
│ ... │ internal/tui ← Bubble Tea model, panes, keybindings
└─────────┘ │
│ │
▼ ▼
internal/config internal/podman ← HTTP-over-Unix-socket Podman client
internal/cli │
(version, commit, │
build date) ▼
│ internal/systemd ← D-Bus / systemctl --user client
│ │
▼ ▼
internal/registry ←─── SQLite database via internal/db
│
▼
internal/healthcheck ← shared declared-vs-running healthcheck drift comparison
cmd/— CLI surface. Each subcommand is a separate file; shared logic lives incmd/common.go,cmd/presenter.go,cmd/upgrade_helper.go, and related helpers.internal/cli/— Build-time version, commit, and build-date strings injected via-ldflagsintocli.Version,cli.Commit, andcli.BuildDate; referenced bycmd.Versionfor--versionand displayed by the TUI loading spinner.internal/tui/— Bubble Tea-based terminal UI (browse.go, panes, keybindings, mouse dispatch, streaming state, and tests). The loading spinner animates at 100ms with a K-2000 (KITT) red half-height bar scanner sweep and shows version/commit/build-date metadata.internal/podman/— thin Podman REST API client with aPodmanClientinterface for testability.internal/prune/— pure, I/O-free computation of which live Podman volumes and images no registered quadlet declares; fails closed on an incomplete or empty quadlet scan.internal/systemd/— systemd user-session client over D-Bus with subprocess fallback tosystemctl --user.internal/registry/— filesystem discovery and registry CRUD againstinternal/db.internal/db/— SQLite schema forpods,images,pod_services,pod_deps, andcontainer_healthchecks.internal/healthcheck/— shared normalization and drift comparison between quadlet-declared and running Podman healthcheck configurations.internal/targets/— target parsing (podorpod:container) and filtering.internal/output/— colored terminal output, tables, JSON rendering, hints, quiet/plain modes, per-stream TTY color detection (stdout vs stderr), and ANSI sanitization of untrusted content.internal/config/— Viper-driven configuration.
TUI guide
Launch the browser with:
pods-ctl browse
Layout
┌────────────────────┬──────────────────────────────────────────┐
│ │ Tab 1: Pod summary │
│ Tree pane │ Tab 2: Container summary │
│ users → pods │ Tab 3: Details │
│ → containers │
│ ├──────────────────────────────────────────┤
│ │ Logs pane (toggle with l) │
│ │ Events pane (toggle with E) │
└────────────────────┴──────────────────────────────────────────┘
- Tree pane (left): navigate users, pods, and containers; inspect live status at a glance.
- Pod/Container/Details panes (right stack): tabbed information.
- Logs/Events panes (bottom tray): streaming views toggled on demand.
Keybindings
| Key | Action |
|---|---|
↑ / ↓ |
Move cursor in the pod tree or Details entries |
→ |
Expand the focused tree node or Details section |
← |
Collapse the focused node, or move to its parent if already collapsed or a leaf |
j / k |
Scroll/move cursor in the focused pane |
Enter / Space |
Expand the focused tree leaf or toggle the focused details section |
a |
Expand all sections (Details) or all tree nodes (Tree) |
x |
Collapse all sections (Details) or all tree nodes (Tree) |
PgUp / PgDn |
Page scroll in Logs/Events and other scrollable panes |
Tab / Shift+Tab |
Cycle pane focus forward/backward |
0–6 |
Focus pane directly: 0=Tree, 1=Pod, 2=Container, 3=Details, 4=Diff, 5=Logs, 6=Events |
l |
Toggle logs pane |
E |
Toggle events pane |
f |
Toggle follow mode on logs/events |
/ |
Search inside the Logs pane (or filter the tree when Logs is not focused) |
n / N |
Next/previous match in the Logs pane search |
s |
Cycle sort mode (name, status, health, CPU, memory) |
r |
Refresh (state-preserving) |
R |
Restart selected container |
S |
Stop selected container |
A |
Start selected container |
u |
Auto-update all pods of the selected user (confirms first) |
g / G |
Upgrade selected container if image mismatch (CLI upgrade also checks healthcheck drift; TUI action checks image drift only) |
d |
Open Diff tab for the selected pod or container |
U |
Undo last container action |
e |
Exec into selected container (Tree pane); exports visible log lines to a file instead (Logs pane) |
p |
Pin/unpin selected pod |
h |
Open health overlay for the selected pod or container |
y |
Copy inspect JSON to clipboard (Details pane) |
space / a / i / x |
Toggle / select all visible / invert / clear selection |
. / Ctrl+a |
Open the bulk action palette |
Ctrl+t / Ctrl+g / Ctrl+/ / Ctrl+w / 7 / P / V / W / I / t / D / C |
Timeline / network / global search / pod wizard / stats / port mappings / volumes / networks / images / process list / disk usage / registry check overlays |
Ctrl+x |
Prune volumes and images not referenced by any registered quadlet (scrollable candidate list, then default-No confirmation) |
Ctrl+r |
Recover the container under the cursor: start if stopped/exited, restart if unhealthy, skip if healthy (no confirmation). With a multi-selection, confirms first (default No); a selected pod recovers its loaded container children only — containers not present in the tree (never created, or the pod service itself is down) are outside the TUI's scope, use CLI pods-ctl recover for those |
f1–f5 |
Quick filters (running/stopped/unhealthy/cycle presets/clear) |
c |
Toggle color |
Esc |
Cancel / close pane / overlay (never quits) |
q |
Close pane / overlay, or quit from the tree (not in text inputs: filter, wizard, global search, help filter) |
! |
Reopen the last error |
? |
Show/hide context-aware help overlay |
Ctrl+C |
Quit from anywhere, including text inputs and modals |
Details-pane navigation: when the Details pane is focused,
↑/↓/j/kmove through JSON tree entries,→expands the current object or array, and←collapses it — or moves to the parent entry if already collapsed or a leaf.Enter/Spacetoggle expansion.a/xexpand/collapse every JSON node.
Footer hints: the bottom bar shows context-aware shortcuts that change based on the currently focused pane (tree, details, logs, events, etc.).
Filtering:
/opens a footer filter bar supportingname:,label:,group:, and plain terms that match name, label, status, and health. Seedocs/guides/browse.mdfor the full query syntax.
Destructive actions show a confirmation dialog; confirm with y/Enter and cancel with n/Esc/q.
Mouse interactions
The TUI supports mouse interactions across all panes and overlays:
| Action | Effect |
|---|---|
| Left-click tree row | Focus tree + move cursor to node |
| Left-click focused tree row | Toggle expand/collapse |
| Double-click pod/container | Open logs pane |
| Right-click tree node | Open action palette for that node |
| Left-click right-stack pane | Focus that pane |
| Left-click Details JSON row | Move JSON cursor / toggle node |
| Left-click logs/events pane | Focus the streaming pane |
| Left-click tab | Switch to that pane |
| Left-click overlay | Dismiss overlay |
| Left-click confirm Yes/No | Select / confirm / cancel |
| Left-click help entry | Select / preview key |
| Left-click palette action | Select / run action |
| Scroll wheel in tree | Move cursor up/down |
| Scroll wheel in panes | Scroll content |
| Scroll wheel on tab bar | Cycle focus |
| Scroll wheel in overlay | Scroll overlay content |
See docs/guides/browse.md for the full mouse reference.
See docs/guides/user-manual.md for a task-oriented walkthrough of common workflows (day-to-day operation, drift detection, bulk actions, troubleshooting) with diagrams.
Development
make all # vet, build, and test
make build # compile pods-ctl with version, commit, and date stamping
make test # run tests with -race
make vet # go vet ./...
make fmt # go fmt ./...
make lint # vet + optional golint/staticcheck
make security # govulncheck — scan dependencies for vulnerabilities
make man # generate man pages to /tmp/pods-ctl-man/
make tidy # go mod tidy && go mod verify
make coverage # generate and open coverage report
make clean # remove compiled binary
See docs/guides/development.md and docs/testing/testing.md for detailed development and testing guidance.
Roadmap
Current milestone: v1.2 Codebase Hardening & Quality
- P0 (complete): Bug fixes — showProperties arg ordering, loadErr not cleared, dryRun bypass in TUI, missing returns after osExit, auditLog memory leak, BaseDir config, withWatch panic safety, lifecycle JSON contract, events.go injectable user, dead code cleanup.
- P1 (complete): Dead code removal and deduplication — removed ~1,100 lines of dead/duplicated code. Consolidated shared utilities into
targetspackage. AddedClose()to podman client. - P2 (pending): Error handling and output — typed error propagation, io.Writer abstraction, context in DB layer, transactions.
- P3 (pending): Test infrastructure — shared fakes, batch-command helper, table-driven tests.
- P4 (pending): Architecture — decompose TUI God Object, wire Keymap, fix race conditions.
- P5 (pending): Refactoring — overlay boilerplate, status/list merge, orphaned entries cleanup.
- P6 (pending): New features —
diff,exec,doctor,history,create, a Prometheus textfile exporter (structuredsloglogging and opt-in pprof/expvar metrics viaPODS_CTL_PPROFalready exist). - UX review (phases 1–4 complete): TUI safety/input fixes, shared rendering foundation (overlay box, units, status classification, light/16-color themes), performance work (on-demand spinner, pane render cache, incremental log filtering, benchmarks), and CLI polish (exit-code contract, dynamic shell completion, command groups and aliases,
logs --since,events --type/--name,prune --volumes, JSONschema_version).
Previous milestone: v1.1 TUI Power User Features (complete)
- Phase 1 (complete): Live logs and Podman events streaming panes with bounded scrollback and clean stream lifecycle.
- Phase 2 (complete): Bulk action palette improvements, log search/filter, image drift Diff pane, health overlay, and CLI
logs/export/importcommands. - Phase 3 (complete): Interactive filter/search panel for the pod tree with footer input, live updates, and prefix-aware queries (
name:,label:,group:). - Phase 4 (complete): Start/stop/restart/refresh actions with confirmation, toast notifications, delayed post-action refresh (3s), and automatic container-reload retry (up to 3 attempts) to handle Podman container recreation latency.
- Phase 5 (complete): Context-aware help overlay and dynamic footer hints.
Deferred to v2.0/polish: live per-container CPU/memory stats pane with sparkline/bar history, structured error wrapping, full dependency-injection migration for SystemdClient/PodmanClient, command-level integration tests, and non-TUI stats/events refinement.
Completed in CLI polish pass: man page generation (pods-ctl man), --quiet/-q, --plain, --no-input, --force on destructive commands, per-command Example: help text, DEBUG/PODS_CTL_NO_COLOR env vars, per-stream TTY color detection, signal handling for events/watch mode, - stdin/stdout convention for export/import, and clig.dev compliance audit.
Out of scope: Kubernetes/OpenShift integration, a web UI or daemon mode, and non-Podman runtimes such as Docker or containerd.
License
License to be determined. See the repository for a LICENSE file once one is added.