fix(fleet): refuse a container start until the mount it binds has answered #109

Merged
dank merged 1 commits from fix/array-mount-gate into main 2026-10-31 18:20:40 -07:00
Owner

Why

Dankserver rebooted with the array's mount and the media containers racing. In a
rootless quadlet RequiresMountsFor= is inert — the user manager cannot
order against the system's mnt-entropy.mount — so podman resolved the bind
against the pre-mount directory. Jellyfin and Plex came up blind, Jellyfin read
an empty library and logged Removing item, Type: "Season" by the thousand, and
nothing refused. Copyparty (Volume=/mnt/entropy:/w:ro) had no guard at all.

Change

  • scripts/wait-for-mount.sh [-t SECONDS] MOUNTPOINT... — waits for each named
    mountpoint, then refuses by name. Exit 0 mounted, 1 refused, 2 usage.
  • jellyfin.container, plex.container, copyparty.container gain
    ExecStartPre=…/scripts/wait-for-mount.sh <sources>; the two media units drop
    UnitConfigItem=ConditionPathIsMountPoint=.
  • Why not the Condition (measured on the host): an unmet condition logs skipped, unmet condition check with NRestarts=0 and no start attempt ever follows,
    so a mount that lands one second late leaves the service dead until a human
    starts it. That is fail-dead. The gate waits, and Restart=always carries it.
  • Law recorded in AGENTS.md (Boot Persistence) and in
    Media Metadata View (guard, law, and the blind-bind failure signature).

Verification

On the host, isolated in a scratch XDG_CONFIG_HOME so no live unit was touched:

  • the podman 5.7.0 generator accepts ExecStartPre in [Service] (exit 0,
    all three units generated, gate line present) — the key is not a batch killer;
  • the gate waits for a bind mount that appears 2 s into its window and proceeds
    (exit 0);
  • it refuses a plain directory after the window, naming the path (exit 1).

New seam tests/array_mount_gate.bats: 6 pass, 1 skip (creating a mount needs
privileges the dev box withholds — that case is the host run above).

The one pre-existing test touched

tests/media_overlay.bats asserted "both media servers bind the view and refuse
to start without it" by grepping the old key by name. Same intent, new mechanism:
the assertion now rides the gate. Nothing else in that file changed.


cwd: /home/blank/.omp/wt/array-gate · session: 2026-10-30T23-38-45-297Z_01a18f2e-18b1-7573-bcd5-ac99636f8bcb

## Why Dankserver rebooted with the array's mount and the media containers racing. In a **rootless** quadlet `RequiresMountsFor=` is inert — the user manager cannot order against the system's `mnt-entropy.mount` — so podman resolved the bind against the *pre-mount* directory. Jellyfin and Plex came up blind, Jellyfin read an empty library and logged `Removing item, Type: "Season"` by the thousand, and nothing refused. Copyparty (`Volume=/mnt/entropy:/w:ro`) had no guard at all. ## Change - `scripts/wait-for-mount.sh [-t SECONDS] MOUNTPOINT...` — waits for each named mountpoint, then refuses **by name**. Exit 0 mounted, 1 refused, 2 usage. - `jellyfin.container`, `plex.container`, `copyparty.container` gain `ExecStartPre=…/scripts/wait-for-mount.sh <sources>`; the two media units drop `UnitConfigItem=ConditionPathIsMountPoint=`. - Why not the Condition (measured on the host): an unmet condition logs `skipped, unmet condition check` with `NRestarts=0` and **no start attempt ever follows**, so a mount that lands one second late leaves the service dead until a human starts it. That is fail-*dead*. The gate waits, and `Restart=always` carries it. - Law recorded in `AGENTS.md` (Boot Persistence) and in [[Media Metadata View]] (guard, law, and the blind-bind failure signature). ## Verification On the host, isolated in a scratch `XDG_CONFIG_HOME` so no live unit was touched: - the podman 5.7.0 generator **accepts** `ExecStartPre` in `[Service]` (exit 0, all three units generated, gate line present) — the key is not a batch killer; - the gate waits for a bind mount that appears 2 s into its window and proceeds (exit 0); - it refuses a plain directory after the window, naming the path (exit 1). New seam `tests/array_mount_gate.bats`: 6 pass, 1 skip (creating a mount needs privileges the dev box withholds — that case is the host run above). ## The one pre-existing test touched `tests/media_overlay.bats` asserted "both media servers bind the view and refuse to start without it" by grepping the old key by name. Same intent, new mechanism: the assertion now rides the gate. Nothing else in that file changed. --- cwd: /home/blank/.omp/wt/array-gate · session: 2026-10-30T23-38-45-297Z_01a18f2e-18b1-7573-bcd5-ac99636f8bcb
dank added 1 commit 2026-10-31 18:18:54 -07:00
Dankserver rebooted with the array's mount and the media containers racing. A
rootless quadlet cannot order itself against the system's mnt-entropy.mount, so
`RequiresMountsFor=/mnt/entropy` is inert in the user manager: podman resolved
the bind against the pre-mount directory, Jellyfin and Plex came up blind, and
Jellyfin read an empty library and began `Removing item, Type: "Season"` by the
thousand. Copyparty had no guard at all.

`scripts/wait-for-mount.sh` is the gate: it waits for every named mountpoint,
then refuses by name. `ConditionPathIsMountPoint=` is NOT the fix — measured on
the host, an unmet condition logs `skipped, unmet condition check` with
NRestarts=0 and no start attempt ever follows, which is fail-dead, not
fail-closed. Jellyfin and Plex swap the condition for the gate; Copyparty gets
one; the law is recorded in AGENTS.md and the view page.

Verified on the host: the 5.7.0 quadlet generator accepts ExecStartPre in
[Service] (exit 0, all three units generated, gate present), the gate waits for
a mount that appears mid-window and proceeds, and it refuses a plain directory
after its window naming the path.

tests/media_overlay.bats pinned the old mechanism by name; its intent — both
media servers refuse to start without the view — now rides the gate.
dank merged commit 6d274acc47 into main 2026-10-31 18:20:40 -07:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#109