From 50a432ee06ca95c8d56bfaf3102d4006523ab971 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 22:23:46 +0300 Subject: [PATCH 1/2] docs: plan tmpfs support --- planning/changes/2026-07-08.05-tmpfs.md | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 planning/changes/2026-07-08.05-tmpfs.md diff --git a/planning/changes/2026-07-08.05-tmpfs.md b/planning/changes/2026-07-08.05-tmpfs.md new file mode 100644 index 0000000..7f4278e --- /dev/null +++ b/planning/changes/2026-07-08.05-tmpfs.md @@ -0,0 +1,48 @@ +--- +summary: Accept the service `tmpfs` key, emitted as podman `--tmpfs [:]`. +--- + +# 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 `` or `:`, 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 ` 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. From e305beb46790468fa159fcaae1a3881e98239743 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 22:31:42 +0300 Subject: [PATCH 2/2] feat: accept and emit the service tmpfs key Compose's tmpfs short syntax (string or list of [:], e.g. /tmp:mode=1777) maps directly onto podman's own --tmpfs CONTAINER-DIR[:OPTIONS] flag (confirmed against podman-run docs), so it's a pure pass-through: no translation, no format validation. --- architecture/supported-subset.md | 14 ++++++++++---- compose2pod/emit.py | 5 +++++ compose2pod/parsing.py | 1 + tests/test_emit.py | 11 +++++++++++ tests/test_parsing.py | 4 ++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/architecture/supported-subset.md b/architecture/supported-subset.md index 254abe2..11ded2b 100644 --- a/architecture/supported-subset.md +++ b/architecture/supported-subset.md @@ -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 `` or + `:` (e.g. `/tmp:mode=1777`), passed through verbatim as + `podman run --tmpfs ` — 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 diff --git a/compose2pod/emit.py b/compose2pod/emit.py index 881403c..e466e28 100644 --- a/compose2pod/emit.py +++ b/compose2pod/emit.py @@ -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 diff --git a/compose2pod/parsing.py b/compose2pod/parsing.py index 4242ee8..f51994f 100644 --- a/compose2pod/parsing.py +++ b/compose2pod/parsing.py @@ -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"} diff --git a/tests/test_emit.py b/tests/test_emit.py index fcc40c5..8362013 100644 --- a/tests/test_emit.py +++ b/tests/test_emit.py @@ -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/"] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 72da17f..59e5f69 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -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) == []