|
| 1 | +--- |
| 2 | +summary: Generated scripts now warn on stderr (non-blocking) when run under podman < 6.0.0, naming the required version and the /etc/hosts pod-wipe bug it fixes. |
| 3 | +--- |
| 4 | + |
| 5 | +# Design: podman version guard in the generated script |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Every script `emit_script` produces gets a small unconditional check, |
| 10 | +inlined into `_SCRIPT_HEADER` right after `set -eu`: it reads the podman |
| 11 | +client's major version and, if it is below 6, prints a two-line warning to |
| 12 | +stderr naming the actual version, the required version, and the underlying |
| 13 | +`/etc/hosts` bug — then continues running the script unchanged. It is a |
| 14 | +diagnostic aid, not a gate. |
| 15 | + |
| 16 | +## Motivation |
| 17 | + |
| 18 | +`README.md` and `architecture/supported-subset.md` already document |
| 19 | +"Requires Podman >= 6.0.0" (this session, debugging a real failure): podman |
| 20 | +before 6.0.0 has a bug where a container stopping inside a multi-container |
| 21 | +pod wipes `/etc/hosts` for every container in the pod, not just the one that |
| 22 | +stopped. compose2pod's pod-level `--add-host` design (see |
| 23 | +`2026-07-13.01-pod-level-add-host.md`) makes every generated script's |
| 24 | +`service_completed_successfully` dependency (e.g. a migration step that runs |
| 25 | +via `podman run --rm` and exits) a trigger for it. The failure this produces |
| 26 | +— `could not translate host name "db" to address` deep inside an unrelated |
| 27 | +application, minutes after the script appeared to succeed — gives no hint |
| 28 | +that podman's version is the cause. A stated README requirement does nothing |
| 29 | +for someone who hits the bug at 2am and has never read the README. The |
| 30 | +script itself is where that information is actually useful. |
| 31 | + |
| 32 | +## Design |
| 33 | + |
| 34 | +`compose2pod/emit.py`'s `_SCRIPT_HEADER` constant gains this block, |
| 35 | +immediately after `set -eu` and before the `wait_healthy()` function |
| 36 | +definition, so it is the first thing every generated script does: |
| 37 | + |
| 38 | +```sh |
| 39 | +podman_version=$(podman version --format '{{.Client.Version}}' 2>/dev/null) || podman_version="unknown" |
| 40 | +podman_major=$(echo "$podman_version" | cut -d. -f1) |
| 41 | +case "$podman_major" in |
| 42 | + ''|*[!0-9]*) ;; # unparseable -- skip rather than false-positive |
| 43 | + *) if [ "$podman_major" -lt 6 ]; then |
| 44 | + echo "warning: podman $podman_version detected; compose2pod requires podman >= 6.0.0" >&2 |
| 45 | + echo "warning: podman < 6.0.0 has a bug where a container stopping in a multi-container pod wipes /etc/hosts for the whole pod (fixed in 6.0.0) -- name resolution between services may fail unpredictably" >&2 |
| 46 | + fi ;; |
| 47 | +esac |
| 48 | +``` |
| 49 | + |
| 50 | +Only the major version is compared: 6.0.0 is an exact upstream fix |
| 51 | +boundary, not a range, so there is no minor/patch granularity to reason |
| 52 | +about. The check is unconditional — it runs for every script regardless of |
| 53 | +how many services are in the target's dependency closure — trading a |
| 54 | +theoretically-avoidable warning on a single-service pod (which cannot hit |
| 55 | +the underlying bug) for one code path with nothing to get wrong. It is |
| 56 | +inlined rather than wrapped in a named function like `wait_healthy()`: |
| 57 | +`wait_healthy` has multiple call sites (one per healthcheck-gated |
| 58 | +dependency), this check has exactly one, so a function would add indirection |
| 59 | +with no reuse to justify it. |
| 60 | + |
| 61 | +The check degrades safely on anything unexpected: if `podman version |
| 62 | +--format` fails or is unsupported, `podman_version` becomes the literal |
| 63 | +string `"unknown"`, `cut -d. -f1` yields `"unknown"`, and the `case` pattern |
| 64 | +`*[!0-9]*` matches it — the check is silently skipped rather than raising |
| 65 | +under `set -eu` or false-warning on a version it could not parse. |
| 66 | + |
| 67 | +It is a warning, not a gate: the script proceeds regardless of the podman |
| 68 | +version detected. A user who has confirmed their specific compose shape |
| 69 | +cannot hit the bug (or is mid-upgrade and accepts the risk) is not blocked. |
| 70 | + |
| 71 | +## Non-goals |
| 72 | + |
| 73 | +- **No escape hatch / suppression env var.** It is a non-blocking warning; |
| 74 | + there is nothing to bypass. Revisit only if this becomes reported as |
| 75 | + noisy in practice. |
| 76 | +- **No closure-shape detection** (e.g. skipping the check for |
| 77 | + single-service targets that can't hit the bug). Rejected for the same |
| 78 | + reason as the escape hatch: one unconditional code path is simpler than a |
| 79 | + correct-in-all-cases shape analysis, and the cost of an unnecessary |
| 80 | + warning is low. |
| 81 | +- **No minor/patch-level version comparison.** The fix boundary is a single |
| 82 | + major-version line (6.0.0); finer comparison buys nothing here. |
| 83 | + |
| 84 | +## Testing |
| 85 | + |
| 86 | +`just test-ci` at 100%: |
| 87 | +- `tests/test_emit.py`: `emit_script(...)` output contains the version-check |
| 88 | + block for a representative compose shape (asserts the `podman version |
| 89 | + --format` line and the `requires podman >= 6.0.0` message are present) — |
| 90 | + one assertion suffices since the block is unconditional and identical |
| 91 | + across every shape. |
| 92 | +- `tests/test_emit.py`: a subprocess-level test extracts just the version |
| 93 | + guard block from `_SCRIPT_HEADER` and runs it under `sh` with a stub |
| 94 | + `podman` script on `PATH` (via a `tmp_path` bin dir prepended to `PATH`) |
| 95 | + that prints a chosen version string. Cases: `5.8.1` → warning lines on |
| 96 | + stderr; `6.0.1` → silent, exit 0; garbage/empty output → silent, exit 0 |
| 97 | + (proves the `set -eu` script doesn't abort on an unparseable version). |
| 98 | + |
| 99 | +`just lint-ci` and `just check-planning` clean. |
| 100 | + |
| 101 | +## Risk |
| 102 | + |
| 103 | +- **False negative on a podman fork/vendor build with a non-numeric or |
| 104 | + differently-shaped `--format` output** (low x low): the `case` guard |
| 105 | + skips the check silently rather than warning wrong or crashing. Accepted: |
| 106 | + matches the "degrade safely" principle above. |
| 107 | +- **Warning noise on CI logs for users already on podman >= 6** (none): the |
| 108 | + check is silent whenever `podman_major -ge 6`, so this only affects users |
| 109 | + actually on the affected versions. |
0 commit comments