No description
  • Go 99.8%
  • Makefile 0.1%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ymage dbee6144c8
All checks were successful
/ build-test-1 (push) Successful in 2m25s
/ release (push) Successful in 25s
/ coverage (push) Successful in 17m36s
/ lint (push) Successful in 4m20s
/ build-test (push) Successful in 9m28s
/ test-race (push) Successful in 1m49s
chore(version): Release v0.53.1
2026-10-08 16:07:26 +02:00
.forgejo chore(ci): bump syft to v1.51.1 2026-09-08 12:26:30 +02:00
cmd feat: restart diagnostics and persistent action history 2026-10-08 14:16:42 +02:00
docs feat: restart diagnostics and persistent action history 2026-10-08 14:16:42 +02:00
internal fix(db): set synchronous=NORMAL to avoid CI test timeouts 2026-10-08 16:04:36 +02:00
.gitignore chore: remove opencode plugin config and prune stale gitignore entries 2026-08-28 02:22:00 +02:00
.tagme-authors.json chore(version): Release v0.46.9 2026-08-10 04:15:16 +02:00
AGENTS.md refactor(prune): share one quadlet-scoped prune engine between callers 2026-09-26 06:22:43 +02:00
CHANGELOG.md chore(version): Release v0.53.1 2026-10-08 16:07:26 +02:00
CLAUDE.md chore(version): Release v0.46.7 2026-08-08 12:38:59 +02:00
cliff.toml chore(cliff): fix default first tag version 2026-09-26 04:45:07 +02:00
go.mod refactor: remove dead code and reduce duplication across cmd/ and internal/ 2026-08-29 00:05:42 +02:00
go.sum chore(deps): bump Go to 1.27.0 and modernc.org/sqlite to v1.57.0 2026-08-28 02:21:56 +02:00
main.go feat(cli): exit codes, dynamic completion, groups, log/event filters 2026-10-08 13:26:31 +02:00
Makefile feat(cli): comprehensive clig.dev compliance pass 2026-07-21 05:43:30 +02:00
README.md feat: restart diagnostics and persistent action history 2026-10-08 14:16:42 +02:00
SECURITY-REVIEW.md feat(prune): also remove exited containers by default 2026-09-27 21:44:00 +02:00
SECURITY.md docs: update documentation for pure-Go SQLite driver migration 2026-07-19 16:52:04 +02:00

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 TUI
  • start [target...], stop [target...], restart [target...] — lifecycle control
  • status [target...] (alias ps) — show pod status and per-container healthcheck configuration, exits non-zero on failures
  • list (alias ls) — registry view with live container count and status
  • discover — scan the configured base directory and populate the registry
  • check — cross-check registry against on-disk .pod files under the configured base directory
  • cleanup — remove stale registry entries
  • validate [target...] — validate registry entries
  • validate-config — validate configuration, registry, and runtime connectivity
  • history [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 recorded
  • diff [target...] — show declared-vs-running image and healthcheck differences without changing anything; exits 1 when anything differs
  • exec <pod[:container]> [-- command...] — run a command or an interactive shell inside a container as the pod's user; returns the command's exit status
  • doctor — diagnose registry, base directory, quadlet drift and per-user setup (account, lingering, systemd session, Podman socket) with a fix hint for each failure
  • health [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 Podman
  • reload [target...] — run systemctl --user daemon-reload
  • recover [target...] — start stopped/exited/unhealthy containers and start missing containers not found in Podman but declared in the registry cache
  • prune [target...] — remove images (and with --volumes, volumes; --all is kept as an alias) that no registered quadlet declares, plus any exited container, per pod user; --broad runs the previous podman system prune -a Default changed: pods-ctl prune no longer runs podman system prune by default — it removes only resources no registered quadlet declares, using the same fail-closed computation and pre-delete re-scan as the TUI's Ctrl+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 --broad for the previous behavior.
  • update [target...] — podman auto-update per pod user
  • pull [target...] — pull all declared container images for pod user(s); works with stopped pods by reading Image= from quadlet .container files via systemd
  • upgrade [target...] — restart pods where the running image or healthcheck config differs from the quadlet
  • refresh-deps [target...] — cache systemd dependency graph and image drift in DB; works with stopped pods
  • remove [target...] (alias rm) — remove pods from the registry and clean up dependencies
  • images [target...] — list collected container images and versions
  • stats [target...] — live CPU/memory/PID/network usage per container
  • events — 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 scripts
  • man <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 inspect output 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 inspect config, 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/.container file 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 quadlet Volume= references, with driver/mountpoint for named volumes
  • Networks overlay (W): the selected pod's quadlet-declared Network= 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's top output 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 of pods-ctl check's stale/unregistered drift and pods-ctl validate's per-entry errors/warnings in one scrollable list with a summary line, loaded on open and refreshed only on r
  • 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's recover command 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:

  1. A config file passed with --config
  2. /etc/pods-ctl/config.*
  3. $HOME/.config/pods-ctl/config.*
  4. 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 in cmd/common.go, cmd/presenter.go, cmd/upgrade_helper.go, and related helpers.
  • internal/cli/ — Build-time version, commit, and build-date strings injected via -ldflags into cli.Version, cli.Commit, and cli.BuildDate; referenced by cmd.Version for --version and 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 a PodmanClient interface 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 to systemctl --user.
  • internal/registry/ — filesystem discovery and registry CRUD against internal/db.
  • internal/db/ — SQLite schema for pods, images, pod_services, pod_deps, and container_healthchecks.
  • internal/healthcheck/ — shared normalization and drift comparison between quadlet-declared and running Podman healthcheck configurations.
  • internal/targets/ — target parsing (pod or pod: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/k move 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/Space toggle expansion. a/x expand/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 supporting name:, label:, group:, and plain terms that match name, label, status, and health. See docs/guides/browse.md for 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 targets package. Added Close() 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 (structured slog logging and opt-in pprof/expvar metrics via PODS_CTL_PPROF already 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, JSON schema_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/import commands.
  • 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.