Files
unraid-podman/plugin/rc.d/rc.podman
maggesandClaude Sonnet 5 51b7262b72
Lint / ShellCheck (push) Successful in 10s
Lint / Validate .plg XML (push) Successful in 10s
Lint / EditorConfig (push) Successful in 4s
Fix five real bugs found by actually installing and running the plugin
First live end-to-end install on real Unraid hardware (all 8 built
packages installed via upgradepkg, rc.podman started, containers
pulled/run/networked/port-mapped) — surfaced five genuine bugs no
amount of container-based CI testing could have caught, since none of
them exist inside the vbatts/slackware:15.0 build container:

1. rc.podman never created $PODMAN_LOG_DIR before redirecting the
   podman system service's output into it, so the service failed to
   even start ("No such file or directory"). Added it alongside the
   existing PODMAN_RUN_DIR mkdir.

2. config/storage.conf hardcoded a [storage] table, and
   podman-config.sh's `sync` step appended a second one at boot with
   the real graphroot/runroot — TOML forbids defining the same table
   twice. Removed the template's [storage] entirely; sync already
   generates the whole thing.

3. config/policy.json had a "_comment" pseudo-field for
   documentation, but containers/image's policy parser rejects any
   unknown top-level key outright. JSON has no comment syntax; moved
   the rationale into docs/ARCHITECTURE.md instead.

4. netavark >= 2.0 dropped its iptables firewall driver entirely
   (verified: passing "iptables" is flatly rejected) — nftables or
   firewalld are the only remaining options, and firewalld needs
   systemd/dbus, which Unraid has neither of. Set firewall_driver =
   "nftables" explicitly and documented that Unraid OS doesn't ship
   the `nft` binary this needs (a slackware64 nftables package works;
   not yet wired into the build/install pipeline — see follow-up).

5. Every container failed with "crun: pivot_root: Invalid argument".
   Root cause: Unraid's / is permanently the kernel's initial "rootfs"
   pseudo-filesystem (Unraid never pivots to a real one at boot — the
   whole OS runs from RAM), and pivot_root(2) unconditionally rejects
   that as the old root. This is not new: Docker/runc hits the exact
   same kernel restriction on this exact host and silently falls back
   to an MS_MOVE-based chroot; crun has no such fallback, only a
   --no-pivot flag with no config-file equivalent. Added
   plugin/sbin/crun-no-pivot.sh, a thin wrapper that scans crun's full
   argument list (podman puts global flags before the subcommand, so
   the subcommand isn't reliably $1) and injects --no-pivot right
   after create/run, and pointed containers.conf's crun runtime at it.
   Also fixed the podman.plg postinstall's chmod glob
   (`podman-*.sh` -> `*.sh`), which would have skipped this new
   non-podman-prefixed sbin script.

Verified end-to-end on the real host: pull, run, real network
connectivity (wget through the container's bridge), and a published
port actually serving HTTP (curl through -p 8099:80 to nginx) all
work. --no-pivot's security tradeoff (disabling one particular
container-escape mitigation) was explicitly discussed with and
approved by the user before committing, given it must be the default
for any container to start at all on this platform.

Follow-up not yet done: nftables (needed for #4) is not yet a
packages/ component in the reproducible build pipeline — it was only
installed manually on the test host for this verification run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 23:06:53 +00:00

245 lines
9.2 KiB
Bash
Executable File

#!/bin/bash
# =============================================================================
# plugin/rc.d/rc.podman
#
# Init script for unraid-podman, in the classic Slackware BSD
# start|stop|restart|status style — Unraid has no systemd, so this script
# alone owns the entire process lifecycle (see docs/ARCHITECTURE.md
# section 6, "Start-/Stop-Skripte", and the top-level "Rahmenbedingungen"
# table on why that's the case).
#
# Staged by plugin/podman.plg to /etc/rc.d/rc.podman. Its boot-time
# invocation is registered (also by podman.plg) in /boot/config/go, gated on
# Unraid's "array started" event rather than running unconditionally at
# boot — the configured storage path (cache pool/disk) is not guaranteed to
# be available before that point. See ARCHITECTURE.md section 6.2.
#
# The individual responsibilities below are deliberately split into small,
# single-purpose scripts under /usr/local/sbin/ (staged from plugin/sbin/)
# rather than inlined here — see each script's own header comment for why
# it's separate:
# podman-preflight.sh startup validation
# podman-config.sh config seeding + sync
# podman-storage.sh podman.img create/mount/unmount
# podman-autostart.sh autostart chain
# podman-backup.sh config/package snapshots for rollback
# podman-verify-packages.sh package integrity checks
# podman-update-packages.sh package reconciliation/update
# podman-uninstall-cleanup.sh called from podman.plg's removepkg block
# =============================================================================
set -u
SBIN_DIR="/usr/local/sbin"
# shellcheck source=../sbin/podman-common.sh
. "$SBIN_DIR/podman-common.sh"
podman_load_cfg
# -----------------------------------------------------------------------------
# podman_start
#
# See file header for the full sequence. Every step is expected to be
# idempotent (safe to re-run `rc.podman start` against an already-running
# instance without breaking anything) — preflight and podman-storage.sh
# both already implement this on their own, but we also short-circuit here
# if the service is clearly already up, to avoid doing redundant work.
# -----------------------------------------------------------------------------
podman_start() {
if [ "${PODMAN_ENABLED:-yes}" != "yes" ]; then
podman_log "start: PODMAN_ENABLED is not 'yes' in podman.cfg, not starting"
return 0
fi
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
podman_log "start: already running (pid $(cat "$PODMAN_SERVICE_PID_FILE"))"
return 0
fi
podman_log "start: beginning startup sequence"
# 1. Preflight — aborts loudly (and already notified) on failure.
if ! "$SBIN_DIR/podman-preflight.sh"; then
podman_log_error "start: preflight checks failed, aborting"
return 1
fi
# 2. Ensure config exists (no-op if already seeded) and sync it to the
# RAM-root /etc/containers/ — see podman-config.sh.
"$SBIN_DIR/podman-config.sh" seed
if ! "$SBIN_DIR/podman-config.sh" sync; then
podman_log_error "start: config sync failed, aborting"
return 1
fi
# 3. Storage: create the image if this is a first start, then mount it.
if ! "$SBIN_DIR/podman-storage.sh" create; then
podman_log_error "start: storage creation failed, aborting"
return 1
fi
if ! "$SBIN_DIR/podman-storage.sh" mount; then
podman_log_error "start: storage mount failed, aborting"
return 1
fi
# 4. Restore persisted netavark network definitions (see
# docs/ARCHITECTURE.md section 8, Netzwerke) from the boot-persistent
# copy into the RAM-root config directory netavark reads from.
mkdir -p "$PODMAN_ETC_DIR/networks"
if [ -d "$PODMAN_NETWORKS_BOOT_DIR" ] && [ -n "$(ls -A "$PODMAN_NETWORKS_BOOT_DIR" 2> /dev/null)" ]; then
cp -a "$PODMAN_NETWORKS_BOOT_DIR"/. "$PODMAN_ETC_DIR/networks/"
podman_log "start: restored custom network definitions"
fi
# 5. Start the Podman API service, rootful, on a unix socket — see
# docs/ARCHITECTURE.md section 6.1 for why this runs as a persistent
# service rather than being started fresh per CLI/WebUI call:
# a shared process gives consistent state and event streaming.
# --time=0 disables the idle-shutdown timeout (this is a long-running
# daemon under our process management, not an on-demand activation).
mkdir -p "$PODMAN_RUN_DIR" "$PODMAN_LOG_DIR"
podman_log "start: starting podman system service on unix://$PODMAN_SOCKET"
nohup podman system service --time=0 "unix://$PODMAN_SOCKET" \
> "$PODMAN_LOG_DIR/podman-service.log" 2>&1 &
local service_pid=$!
echo "$service_pid" > "$PODMAN_SERVICE_PID_FILE"
# Wait for the socket to actually appear rather than assuming the fork
# succeeded instantly — up to 15s, polled every 200ms.
local waited=0
while [ ! -S "$PODMAN_SOCKET" ] && [ "$waited" -lt 15000 ]; do
sleep 0.2
waited=$((waited + 200))
if ! kill -0 "$service_pid" 2> /dev/null; then
podman_log_error "start: podman system service exited immediately — see $PODMAN_LOG_DIR/podman-service.log"
"$SBIN_DIR/podman-storage.sh" unmount || true
podman_notify "Podman failed to start" \
"podman system service exited immediately. See $PODMAN_LOG_DIR/podman-service.log." \
"alert"
return 1
fi
done
if [ ! -S "$PODMAN_SOCKET" ]; then
podman_log_error "start: timed out waiting for $PODMAN_SOCKET to appear"
return 1
fi
podman_log "start: podman system service is up (pid $service_pid)"
# 6. Autostart. A failure here is logged/notified by the script itself
# and does not abort rc.podman start — the service is already usable.
"$SBIN_DIR/podman-autostart.sh" || podman_log_error "start: autostart chain reported errors (see above)"
echo "running" > "$PODMAN_STATUS_FILE"
podman_log "start: startup sequence complete"
return 0
}
# -----------------------------------------------------------------------------
# podman_stop
#
# Stops containers first (each with its own STOP_TIMEOUT grace period),
# then the API service, then unmounts storage — the reverse of start, so
# nothing is torn down out from under something still using it.
# -----------------------------------------------------------------------------
podman_stop() {
if [ ! -S "$PODMAN_SOCKET" ]; then
podman_log "stop: not running (no socket at $PODMAN_SOCKET)"
# Still attempt an unmount in case a prior stop was interrupted after
# the service went down but before the unmount completed.
"$SBIN_DIR/podman-storage.sh" unmount || true
rm -f "$PODMAN_STATUS_FILE"
return 0
fi
podman_log "stop: stopping running containers (timeout ${STOP_TIMEOUT:-10}s each)"
local running_ids
running_ids=$(podman --url "unix://$PODMAN_SOCKET" ps -q 2> /dev/null || true)
if [ -n "$running_ids" ]; then
local id
for id in $running_ids; do
podman --url "unix://$PODMAN_SOCKET" stop -t "${STOP_TIMEOUT:-10}" "$id" \
> /dev/null 2>&1 \
|| podman_log_error "stop: failed to stop container $id within timeout"
done
fi
podman_log "stop: stopping podman system service"
if [ -f "$PODMAN_SERVICE_PID_FILE" ]; then
local pid
pid=$(cat "$PODMAN_SERVICE_PID_FILE")
if kill -0 "$pid" 2> /dev/null; then
kill "$pid" 2> /dev/null || true
local waited=0
while kill -0 "$pid" 2> /dev/null && [ "$waited" -lt 10 ]; do
sleep 1
waited=$((waited + 1))
done
kill -0 "$pid" 2> /dev/null && kill -9 "$pid" 2> /dev/null || true
fi
rm -f "$PODMAN_SERVICE_PID_FILE"
fi
rm -f "$PODMAN_SOCKET"
"$SBIN_DIR/podman-storage.sh" unmount || podman_log_error "stop: storage unmount failed"
rm -f "$PODMAN_STATUS_FILE"
podman_log "stop: stopped"
return 0
}
# -----------------------------------------------------------------------------
# podman_status
#
# Human-readable health summary — see docs/ARCHITECTURE.md section 16.2.
# -----------------------------------------------------------------------------
podman_status() {
echo "PODMAN_ENABLED: ${PODMAN_ENABLED:-yes}"
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
echo "service: running (pid $(cat "$PODMAN_SERVICE_PID_FILE"), socket $PODMAN_SOCKET)"
else
echo "service: stopped"
fi
echo
"$SBIN_DIR/podman-storage.sh" status
echo
if [ -f "$PODMAN_AUTOSTART_FILE" ]; then
local count
count=$(grep -vcE '^\s*(#|$)' "$PODMAN_AUTOSTART_FILE" 2> /dev/null || echo 0)
echo "autostart entries: $count (see $PODMAN_AUTOSTART_FILE)"
fi
if [ -S "$PODMAN_SOCKET" ]; then
echo
echo "podman info:"
podman --url "unix://$PODMAN_SOCKET" info --format \
' containers: {{.Store.ContainerStore.Number}} images: {{.Store.ImageStore.Number}}' \
2> /dev/null || echo " (failed to query — service may still be initializing)"
fi
}
case "${1:-}" in
start)
podman_start
;;
stop)
podman_stop
;;
restart)
podman_stop
podman_start
;;
status)
podman_status
;;
*)
echo "usage: $0 {start|stop|restart|status}"
exit 1
;;
esac