|
| 1 | +--- |
| 2 | +summary: Accept the long-form `volumes` `type: image` mount — `{source (required), target, read_only, image: {subpath}}` — emitting `podman run --mount type=image,...`, closing the last podman-expressible conformance over-rejection; `read_only` is accept-and-ignored on an image mount (podman image mounts are inherently read-only), and `source` is required (a rule-two narrowing, since podman refuses a sourceless image mount). |
| 3 | +--- |
| 4 | + |
| 5 | +# Design: long-form volumes `type: image` mount |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +The long-form `volumes` work (`2026-07-16.05`, `2026-07-17.02`) accepts |
| 10 | +`type` in `bind`/`volume`/`tmpfs` but refuses `type: image` — a deferred parser |
| 11 | +gap (`planning/deferred.md`): Docker accepts it and podman *can* express it. This |
| 12 | +adds `image` as a fourth supported type, emitting `podman run --mount |
| 13 | +type=image,source=<ref>,target=<path>[,subpath=<sub>]`. It closes the last |
| 14 | +podman-expressible over-rejection in the conformance harness. |
| 15 | + |
| 16 | +## Motivation |
| 17 | + |
| 18 | +`type: image` mounts an image's rootfs into the container. Measured against |
| 19 | +`docker compose config` v5.1.2 and podman 6.0.1: Docker accepts it and `podman |
| 20 | +run --mount type=image,source=busybox:1.36,target=/img` succeeds. It is the only |
| 21 | +remaining refused-but-podman-expressible long-form volume shape. |
| 22 | + |
| 23 | +## Design |
| 24 | + |
| 25 | +### Grammar (measured) |
| 26 | + |
| 27 | +An `image` entry is a long-form mapping like the others, with these per-type |
| 28 | +rules: |
| 29 | + |
| 30 | +- **`source`** — **required** string (the image reference). Docker accepts a |
| 31 | + sourceless image entry at config, but podman refuses one (`must set source and |
| 32 | + destination for image volume`), so compose2pod requires it — a rule-two |
| 33 | + narrowing, the same stance `bind` takes. |
| 34 | +- **`target`** — required, absolute (the existing checks apply unchanged). |
| 35 | +- **`read_only`** — accepted (bool, via `values.is_bool_like`) but **not |
| 36 | + emitted**. An image mount is inherently read-only; podman refuses both a |
| 37 | + `readonly` and an `rw` option on it. Docker accepts `read_only: true`/`false` |
| 38 | + and ignores it; compose2pod matches — validated, never appended as `ro`. (No |
| 39 | + over-rejection: `true` is a redundant no-op, `false` a harmless one.) |
| 40 | +- **`image: {subpath}`** — the nested option map for an image entry (measured: |
| 41 | + strict, `subpath` only) → `subpath=<v>`. A sub-map not matching `type: image` |
| 42 | + is refused, per the existing mismatched-sub-map rule (`2026-07-17.02`). |
| 43 | + |
| 44 | +### Validation (`parsing.py`) |
| 45 | + |
| 46 | +- Add `"image"` to `_VOLUME_LONG_TYPES`. |
| 47 | +- `_validate_volume_long_form_source`: require a string `source` for `image` (the |
| 48 | + same branch as `bind`). |
| 49 | +- Add `"image": {"subpath"}` to `_VOLUME_OPTION_KEYS`; the image sub-map validator |
| 50 | + checks `subpath` is a string (reuse the `volume` sub-map's `subpath` check). |
| 51 | + |
| 52 | +### Emit (`emit._mount_flag` / `_nested_mount_parts`) |
| 53 | + |
| 54 | +- `_mount_flag`: an `image` entry emits `type=image,source=<S>,target=<T>` and, |
| 55 | + from the `image:` sub-map, `subpath=<v>`. **Skip the `ro` append for `image`** — |
| 56 | + podman rejects the option; the mount is read-only regardless. |
| 57 | +- `_nested_mount_parts`: the `image` branch emits `subpath` from the image sub-map |
| 58 | + (identical spelling to `volume`'s `subpath`). |
| 59 | + |
| 60 | +## Non-goals |
| 61 | + |
| 62 | +- **A sourceless image mount** — refused (podman requires source). Rule-two, |
| 63 | + measured. |
| 64 | +- **`read_only` changing the mount's read-only-ness** — an image mount is always |
| 65 | + read-only; `read_only` is validated but inert, matching Docker. |
| 66 | +- **A mismatched sub-map on an image entry** (`bind:` on `type: image`) — refused, |
| 67 | + per the existing stricter-than-Docker rule. |
| 68 | + |
| 69 | +## Testing (TDD; Docker + podman oracles) |
| 70 | + |
| 71 | +- **parsing**: accept `{type: image, source, target}`, with `read_only: |
| 72 | + true`/`false`, and with `image: {subpath}`; reject a sourceless image entry, an |
| 73 | + unknown `image:` sub-map key, a non-string `subpath`, and a mismatched sub-map |
| 74 | + (`bind:` on `type: image`). |
| 75 | +- **emit**: `type=image,source=<S>,target=<T>`; with `subpath` appended; and |
| 76 | + crucially **no `ro`** even when `read_only: true`. |
| 77 | +- **conformance**: a `type: image` corpus doc flips over-reject → `both-accept` |
| 78 | + (dedicated assertion). |
| 79 | +- **integration**: a `type: image` mount round-trips on real podman (mount an |
| 80 | + image's rootfs and read a file from it). |
| 81 | +- **promotion**: remove the `image` bullet from `planning/deferred.md`; update |
| 82 | + `architecture/supported-subset.md` (image now supported). |
| 83 | +- `just test-ci` @ 100% coverage, `just lint-ci`, `just check-planning`, |
| 84 | + `just test-conformance`. |
| 85 | + |
| 86 | +## Risk |
| 87 | + |
| 88 | +- **The `ro`-skip-for-image is missed** (low × high): a `read_only: true` image |
| 89 | + entry would emit `type=image,...,ro`, which podman refuses at run time. |
| 90 | + Mitigated by the explicit no-`ro` emit test and the integration round-trip. |
| 91 | +- **`source` required-ness diverges** (low × low): measured — podman refuses a |
| 92 | + sourceless image mount, so requiring it is exact rule-two parity, not an |
| 93 | + over-rejection beyond the sourceless case. |
| 94 | +- **A docker `image:` sub-map key beyond `subpath`** (low × low): measured only |
| 95 | + `subpath`; another key would be refused as unknown (a tracked over-reject if one |
| 96 | + exists), never a false green. |
0 commit comments