Skip to content

Commit 5bed4db

Browse files
authored
fix: anonymous volumes crash the emitter (#11)
* docs: plan anonymous volume support * fix: emit anonymous volumes as -v <path> instead of crashing A single container-path volume (no source:target, e.g. /var/cache/models) passed validate() but crashed run_flags at volume.split(':', 1). Emit it as an anonymous volume (-v <path>) with no host-path translation, matching Compose and podman semantics. * fix: reject relative colon-less volumes loudly An anonymous volume must be an absolute container path. A colon-less entry that is not absolute (e.g. ./cache) is malformed Compose; validate() now raises instead of letting emit produce a broken -v ./cache. * docs: add 0.1.3 release notes * docs: finalize anonymous-volume change summary
1 parent 892ec0e commit 5bed4db

7 files changed

Lines changed: 118 additions & 0 deletions

File tree

‎architecture/supported-subset.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ warns (ignored, behavior-neutral inside a single pod) or raises
4747
Short bind-mount syntax only (`source:target`). The source must be a host path
4848
(starts with `.` or `/`); named volumes and the long mapping form raise.
4949

50+
A single absolute container path with no `source:target` (e.g.
51+
`- /var/cache/models`) is accepted as an **anonymous volume** and emitted
52+
verbatim as `-v <path>` — podman creates an anonymous volume at that path (the
53+
common way to shadow a subdirectory of a bind mount). No host-path translation
54+
is applied, since the entry names a container path, not a host source. A
55+
colon-less entry that is not absolute (e.g. `./cache`) is malformed and raises.
56+
5057
## depends_on
5158

5259
All three conditions are honored: `service_started`, `service_healthy`,

‎compose2pod/emit.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def run_flags(name: str, svc: dict[str, Any], pod: str, hosts: list[str], projec
5757
for env_file in env_files:
5858
flags += ["--env-file", str(Path(project_dir, env_file))]
5959
for volume in svc.get("volumes") or []:
60+
if ":" not in volume:
61+
# Anonymous volume: a bare container path, no host source to translate.
62+
flags += ["-v", volume]
63+
continue
6064
source, destination = volume.split(":", 1)
6165
if not source.startswith("/"):
6266
source = str(Path(project_dir, source))

‎compose2pod/parsing.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ def _validate_service_volumes(name: str, svc: dict[str, Any]) -> None:
4141
if not isinstance(volume, str):
4242
msg = f"service {name!r}: only short volume syntax is supported"
4343
raise UnsupportedComposeError(msg)
44+
if ":" not in volume:
45+
# Anonymous volume: must be an absolute container path.
46+
if not volume.startswith("/"):
47+
msg = f"service {name!r}: anonymous volume '{volume}' must be an absolute path"
48+
raise UnsupportedComposeError(msg)
49+
continue
4450
source = volume.split(":", 1)[0]
4551
if not source.startswith((".", "/")):
4652
msg = f"service {name!r}: named volume '{source}' is not supported (bind mounts only)"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
summary: Emit absolute anonymous volumes as `-v <path>` instead of crashing; reject non-absolute colon-less entries loudly.
3+
---
4+
5+
# Change: Support anonymous volumes
6+
7+
**Lane:** lightweight — one source file (`emit.py`), a resolved design choice,
8+
two tests, plus the capability-doc promotion.
9+
10+
## Goal
11+
12+
A Compose short-form volume can be a single container path with no
13+
`source:target` (an *anonymous volume*, e.g. `- /var/cache/models`). Such an
14+
entry passes `validate()` (its path starts with `/`) but crashes
15+
`emit.run_flags` at `volume.split(":", 1)` with
16+
`ValueError: not enough values to unpack (expected 2, got 1)`. This shipped in
17+
0.1.2 and breaks real compose files that use anonymous volumes to shadow
18+
subdirectories of a bind mount. Fix: emit an anonymous volume as `-v <path>`.
19+
20+
## Approach
21+
22+
In `run_flags` (`compose2pod/emit.py`), branch on whether the volume string
23+
contains a `:`. No colon → anonymous volume: append `["-v", volume]` verbatim
24+
(no `project_dir` translation — an anonymous volume names a container path, not
25+
a host source) and continue. With a colon → the existing bind-mount path
26+
(source translated against `project_dir` when relative).
27+
28+
Design choice (resolved): **support**, not reject. Anonymous volumes are valid
29+
Compose; podman's `-v <path>` creates an anonymous volume at that path, which is
30+
exactly the compose semantics. Rejecting in `validate()` would fix the crash but
31+
break compose files that rely on them.
32+
33+
`_validate_service_volumes` also gains a guard: a colon-less entry that is *not*
34+
absolute (e.g. `./cache`) is malformed Compose and now raises
35+
`UnsupportedComposeError` rather than slipping through to emit a broken
36+
`-v ./cache`. This keeps the project's "refuse loudly, never emit a silently
37+
broken script" contract. Absolute anonymous volumes (`/var/cache/models`)
38+
continue to be accepted.
39+
40+
Promote into `architecture/supported-subset.md`: the Volumes section currently
41+
says "Short bind-mount syntax only (`source:target`)" — extend it to note that a
42+
single absolute container path is accepted as an anonymous volume and emitted as
43+
`-v <path>`.
44+
45+
## Files
46+
47+
- `compose2pod/emit.py` — `run_flags`: handle the no-colon (anonymous) volume case.
48+
- `tests/test_emit.py` — anonymous volume emits `-v <path>`; regression for the crash.
49+
- `tests/test_parsing.py` — anonymous volume passes `validate()` (pins the accept).
50+
- `architecture/supported-subset.md` — Volumes section promotion.
51+
52+
## Verification
53+
54+
- [ ] Failing test first: `run_flags` with `volumes: ["/var/cache/models"]` — expected RED `ValueError: not enough values to unpack`.
55+
- [ ] Apply the `emit.py` branch.
56+
- [ ] Test passes: the emitted flags contain `["-v", "/var/cache/models"]`.
57+
- [ ] `just test-ci` — full suite green at 100% line coverage.
58+
- [ ] `just lint-ci` — clean (ruff ALL, ruff format, ty, eof-fixer, planning check).

‎planning/releases/0.1.3.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# compose2pod 0.1.3 — anonymous volumes no longer crash
2+
3+
A patch release fixing a crash on Compose files that use anonymous volumes. A
4+
short-form volume that is a single container path with no `source:target` (e.g.
5+
`- /var/cache/models`) passed validation but then crashed the emitter with
6+
`ValueError: not enough values to unpack`. Such files now convert.
7+
8+
## Fix
9+
10+
- **Anonymous volumes are emitted as `-v <path>`.** A colon-less volume entry
11+
is a Compose anonymous volume; it is now rendered verbatim as `podman run -v
12+
<path>` (podman creates an anonymous volume at that container path) instead of
13+
crashing `run_flags`. No host-path translation is applied — the entry names a
14+
container path, not a host source.
15+
- **Malformed colon-less volumes are rejected loudly.** A colon-less entry that
16+
is not absolute (e.g. `./cache`) is invalid Compose and now raises a clear
17+
`unsupported`-style error instead of silently emitting a broken `-v ./cache`,
18+
keeping compose2pod's "refuse loudly, never emit a silently broken script"
19+
contract.
20+
21+
## Downstream
22+
23+
No action needed — additive and backward compatible. Compose files that
24+
previously crashed with `ValueError: not enough values to unpack` on an
25+
anonymous volume now emit a pod script; nothing that converted before changes.
26+
27+
## Internals
28+
29+
- `architecture/supported-subset.md` documents anonymous volumes in the Volumes
30+
matrix (absolute accepted and emitted as `-v <path>`; non-absolute raises).
31+
- 89 tests at 100% line coverage (enforced); `ruff select=ALL`, `ty`, and
32+
`eof-fixer` clean.

‎tests/test_emit.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def test_absolute_volume_source_is_kept_as_is(self) -> None:
4949
flags = run_flags("app", {"image": "x", "volumes": ["/data/app:/srv/www/"]}, "p", [], "/builds/x")
5050
assert flags[4:6] == ["-v", "/data/app:/srv/www/"]
5151

52+
def test_anonymous_volume_emitted_as_single_path(self) -> None:
53+
flags = run_flags("app", {"image": "x", "volumes": ["/var/cache/models"]}, "p", [], "/builds/x")
54+
assert flags[4:6] == ["-v", "/var/cache/models"]
55+
5256
def test_healthcheck_without_timeout_omits_health_timeout_flag(self) -> None:
5357
flags = run_flags("app", {"image": "x", "healthcheck": {"test": "true"}}, "p", [], "/builds/x")
5458
assert flags[4:6] == ["--health-cmd", "true"]

‎tests/test_parsing.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def test_long_volume_syntax_raises(self) -> None:
3535
with pytest.raises(UnsupportedComposeError, match="short volume syntax"):
3636
validate(compose)
3737

38+
def test_anonymous_volume_is_accepted(self) -> None:
39+
assert validate({"services": {"app": {"image": "x", "volumes": ["/var/cache/models"]}}}) == []
40+
41+
def test_relative_anonymous_volume_raises(self) -> None:
42+
with pytest.raises(UnsupportedComposeError, match="absolute"):
43+
validate({"services": {"app": {"image": "x", "volumes": ["./cache"]}}})
44+
3845
def test_no_services_raises(self) -> None:
3946
with pytest.raises(UnsupportedComposeError, match="no services"):
4047
validate({"services": {}})

0 commit comments

Comments
 (0)