Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions architecture/supported-subset.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ warns (ignored, behavior-neutral inside a single pod) or raises

- **Supported:** `image`, `build`, `command`, `environment`, `env_file`,
`volumes`, `healthcheck`, `depends_on`, `networks`, `hostname`,
`container_name`. compose2pod never builds: a `build` section is accepted
but its contents (context, dockerfile, args) are not read — `image_for`
(`compose2pod/emit.py`) runs the CI image supplied via `--image` for any
service that has one.
`container_name`, `tmpfs`. compose2pod never builds: a `build` section is
accepted but its contents (context, dockerfile, args) are not read —
`image_for` (`compose2pod/emit.py`) runs the CI image supplied via `--image`
for any service that has one.
- **`tmpfs`:** a string or list of strings, each `<path>` or
`<path>:<options>` (e.g. `/tmp:mode=1777`), passed through verbatim as
`podman run --tmpfs <value>` — Compose's short syntax maps directly onto
podman's own `--tmpfs CONTAINER-DIR[:OPTIONS]` flag, so no translation is
needed. No format validation; a malformed option string surfaces as a
podman error at run time.
- **`hostname` and `container_name`:** both are made resolvable to
`127.0.0.1` like a network alias (added to the shared `--add-host` set), so
other services can reach the service by either name. The pod shares the UTS
Expand Down
5 changes: 5 additions & 0 deletions compose2pod/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ def run_flags(name: str, svc: dict[str, Any], pod: str, hosts: list[str], projec
# Absolute bind mount (starts with "/") and named volume (bare
# identifier) are both kept as-is — neither is a path to translate.
flags += ["-v", f"{source}:{destination}"]
tmpfs = svc.get("tmpfs") or []
if isinstance(tmpfs, str):
tmpfs = [tmpfs]
for mount in tmpfs:
flags += ["--tmpfs", mount]
healthcheck = svc.get("healthcheck") or {}
_add_health_flags(flags, healthcheck)
return flags
Expand Down
1 change: 1 addition & 0 deletions compose2pod/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"networks",
"hostname",
"container_name",
"tmpfs",
}
IGNORED_SERVICE_KEYS = {"ports", "restart", "stdin_open", "tty"}
SUPPORTED_HEALTHCHECK_KEYS = {"test", "interval", "timeout", "retries", "start_period"}
Expand Down
48 changes: 48 additions & 0 deletions planning/changes/2026-07-08.05-tmpfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
summary: Accept the service `tmpfs` key, emitted as podman `--tmpfs <path>[:<options>]`.
---

# Change: Support the service `tmpfs` key

**Lane:** lightweight — two files (`parsing.py`, `emit.py`), no new file, no
public-API change, single straightforward pass-through (verified against
podman's own flag docs, no design ambiguity).

## Goal

`validate()` rejects the service `tmpfs` key
(`service 'agent-worker': unsupported key 'tmpfs'`). Compose's `tmpfs:` short
syntax (a string or list of strings, each `<path>` or `<path>:<options>`, e.g.
`/tmp:mode=1777`) maps directly onto podman's own `--tmpfs
CONTAINER-DIR[:OPTIONS]` flag — confirmed against podman's `podman-run` docs:
"The general format is: `--tmpfs CONTAINER-DIR[:OPTIONS]`" with the same
`mode=`/`size=` option vocabulary Compose uses. This is a pure pass-through,
unlike volumes: no host-path translation, no bind-vs-named disambiguation.

## Approach

- `parsing.py`: add `"tmpfs"` to `SUPPORTED_SERVICE_KEYS`. No dedicated
validation — `tmpfs` is a string or list of strings; malformed option
strings surface as a podman error at run time, consistent with how other
pass-through values (image names, volume names) are handled.
- `emit.py` (`run_flags`): normalize `svc.get("tmpfs")` to a list the same way
`env_file` already is (`str` → single-element list), then append
`["--tmpfs", value]` for each entry, verbatim.
- Promote into `architecture/supported-subset.md`.

## Files

- `compose2pod/parsing.py` — add `tmpfs` to `SUPPORTED_SERVICE_KEYS`.
- `compose2pod/emit.py` — `run_flags`: emit `--tmpfs <value>` per entry.
- `tests/test_parsing.py` — `tmpfs` (string and list forms) validates.
- `tests/test_emit.py` — `run_flags` emits `--tmpfs` for both forms.
- `architecture/supported-subset.md` — Service keys promotion.

## Verification

- [ ] Failing test first: `run_flags` with `tmpfs: ["/tmp:mode=1777"]` — no
`--tmpfs` flag present (RED).
- [ ] Apply the `parsing.py` + `emit.py` changes.
- [ ] Test passes: `["--tmpfs", "/tmp:mode=1777"]` present in flags.
- [ ] `just test-ci` — full suite green at 100% line coverage.
- [ ] `just lint-ci` — clean.
11 changes: 11 additions & 0 deletions tests/test_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def test_env_file_list_form(self) -> None:
flags = run_flags("app", svc, "p", [], "/builds/x")
assert flags[4:8] == ["--env-file", "/builds/x/a.env", "--env-file", "/builds/x/b.env"]

def test_tmpfs_string_form(self) -> None:
# S108 flags "/tmp" as an insecure hardcoded temp path; this is a
# pass-through string being tested, not a file write.
flags = run_flags("app", {"image": "x", "tmpfs": "/tmp:mode=1777"}, "p", [], "/builds/x") # noqa: S108
assert flags[4:6] == ["--tmpfs", "/tmp:mode=1777"] # noqa: S108

def test_tmpfs_list_form(self) -> None:
svc = {"image": "x", "tmpfs": ["/tmp:mode=1777", "/run"]} # noqa: S108
flags = run_flags("app", svc, "p", [], "/builds/x")
assert flags[4:8] == ["--tmpfs", "/tmp:mode=1777", "--tmpfs", "/run"] # noqa: S108

def test_absolute_volume_source_is_kept_as_is(self) -> None:
flags = run_flags("app", {"image": "x", "volumes": ["/data/app:/srv/www/"]}, "p", [], "/builds/x")
assert flags[4:6] == ["-v", "/data/app:/srv/www/"]
Expand Down
4 changes: 4 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ def test_service_hostname_is_accepted(self) -> None:
def test_service_container_name_is_accepted(self) -> None:
compose = {"services": {"application": {"image": "x", "container_name": "calutron-ronline"}}}
assert validate(compose) == []

def test_service_tmpfs_is_accepted(self) -> None:
compose = {"services": {"app": {"image": "x", "tmpfs": ["/tmp:mode=1777"]}}} # noqa: S108
assert validate(compose) == []