Releases: modern-python/compose2pod
Release list
0.1.8
compose2pod 0.1.8 — interpolation moves to run time
This release reverses 0.1.7: Compose ${VAR} interpolation now happens when
the generated script runs, not when compose2pod generates it. compose2pod
emits a script that runs later — typically in CI, where the real secrets and
config live — so resolving against compose2pod's own environment at generation
time baked any variable that was unset then to an empty string. References are
now emitted as live POSIX-shell fragments that the runtime shell expands.
Change
${VAR}is interpolated at script-run time, not generation time.
to_shell()(newcompose2pod/shell.py) re-encodes every compose-derived
value (environment,image,command,volumes,tmpfs,env_file,
healthchecktest) into a double-quoted shell fragment whose variable
references stay live, so the generated script's own shell resolves them
against its runtime environment. All the 0.1.7 forms still work —$VAR,
${VAR},${VAR:-default},${VAR-default},${VAR:?msg},${VAR?msg},
${VAR:+alt},${VAR+alt},$$— mapping onto identical POSIXsh
parameter expansion. A bare$VAR/${VAR}is emitted as${VAR-}so an
unset variable expands to empty under the script'sset -euinstead of
aborting;${VAR:?msg}now fails the script at run time (not at generation).
Command substitution and other shell metacharacters in literal text are
escaped, so nothing but a variable reference is ever live. The CLI prints one
informational note listing the variables the script references at run time.
Fix
environmentnull value is host passthrough. A null mapping value
(KEY:) now emits a bare-e KEY— passingKEYthrough from the host,
identical to the list form- KEY— instead of the literal-e KEY=None.- Malformed braced references are rejected. A braced reference whose text
after the name is not a valid operator (e.g.${FOO!bar}) now raises a clear
UnsupportedComposeErrorinstead of silently dropping the trailing text.
Packaging
- Public API: the generation-time
interpolatefunction is removed;
to_shellis exported in its place.to_shell(value)returns a
runtime-expandable shell fragment, not a resolved value.
Why
The 0.1.7 model resolved ${VAR} against os.environ at generation time. For
compose2pod's actual use — generate a script now, run it later in CI — that was
the wrong moment: the variables are supposed to be unset at generation and set
at run time, so 0.1.7 baked them to empty and emitted a spurious "variable not
set" warning. Deferring to the runtime shell resolves them where they exist.
Downstream
- Compose files: set
${VAR}values in the runtime environment of the
generated script (e.g. your CI job), not in the environment that runs
compose2pod. An unset bare${VAR}now expands to empty at run time; write
${VAR:?message}where you want a missing variable to fail the run. - API consumers:
compose2pod.interpolateis gone. Use
compose2pod.to_shell(value), which emits a runtime-expandable shell fragment
rather than resolving the value in-process.
Internals
architecture/supported-subset.mdrewritten to describe run-time expansion,
the${VAR-}/$$/${VAR:?}semantics,env_fileinterpolation, the
reference note, null-value passthrough, and malformed-reference rejection.- 123 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.test_shell.pyexecutes emitted fragments under real
sh -eucto prove run-time expansion, the${VAR:?}abort, and
command-substitution inertness.
0.1.7
compose2pod 0.1.7 — variable interpolation
A patch release resolving Compose-spec ${VAR} variable interpolation
against the process environment. Previously, a compose file using ${VAR}
reached the container as the literal placeholder text — shlex.quote
correctly single-quoted the value for safe script embedding, which also
meant the shell never expanded it, so nothing in the pipeline ever
substituted the real value. Compose files that lean on host/CI environment
values now convert and run correctly, matching docker compose.
Fix
${VAR}-style interpolation is resolved. Every string leaf of the
compose document is checked againstos.environbefore validation:$VAR,
${VAR},${VAR:-default},${VAR-default},${VAR:?msg},${VAR?msg},
${VAR:+alt},${VAR+alt}, and$$(literal$) are all supported. An
unset$VAR/${VAR}with no default resolves to an empty string and
prints a warning rather than failing;${VAR:?msg}/${VAR?msg}raises a
clear error instead of silently defaulting.
Downstream
No action needed — additive and backward compatible. A document with no
$-prefixed values is unaffected; one that already relied on the previous
literal pass-through (unlikely, since it's not useful behavior) would now see
the resolved value instead.
Internals
architecture/supported-subset.mddocuments the supported interpolation
forms and the explicit non-goal: no.envfile loading, only the caller's
existing environment is consulted.- 123 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.6
compose2pod 0.1.6 — accept the service tmpfs key
A patch release that stops rejecting valid compose documents which set a
service tmpfs. The validator was over-strict: a service carrying tmpfs: —
a string or list of <path>[:<options>] entries, e.g. /tmp:mode=1777 —
raised unsupported key 'tmpfs'. Such documents now convert.
Fix
- The service
tmpfskey is accepted.validate()no longer rejects a
service that setstmpfs:. Each entry is emitted verbatim aspodman 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.
Downstream
No action needed — additive and backward compatible. Documents that previously
failed with unsupported key 'tmpfs' now emit a pod script; nothing that
converted before changes.
Internals
architecture/supported-subset.mddocumentstmpfsin the Service keys
matrix.- 98 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.5
compose2pod 0.1.5 — named volumes
A patch release accepting Compose named volumes, previously a hard error.
Compose files that declare a top-level volumes: block or reference a named
volume in a service (e.g. pgdata:/var/lib/postgresql/data for database data
persistence) now convert without editing the compose file.
Fix
- Named volumes are accepted. A service volume whose source is a bare
identifier rather than a host path (a Compose named volume) is emitted
verbatim as-v <name>:<target>. Podman creates the volume implicitly on
first reference — the same behavior as plainpodman run -v— so no
explicitpodman volume createstep is needed. The volume persists on the
host after the pod is removed, identical todocker compose downwithout
-v. - Top-level
volumes:is accepted (ignored). Mirrors the existing
top-levelnetworkstreatment: the block'sdriver/driver_optsand
externalsettings are never read, since implicit creation with default
options is what podman does regardless. A non-default driver or an
external: truevolume that should pre-exist has no special handling.
Downstream
No action needed — additive and backward compatible. Documents that previously
failed with unsupported top-level keys: ['volumes'] or "named volume ... is
not supported" now emit a pod script; nothing that converted before changes.
Internals
architecture/supported-subset.mddocuments the bind-mount vs named-volume
disambiguation (source starts with.//vs bare identifier) and the
default-driver-only limitation.- 95 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.4
compose2pod 0.1.4 — accept the service container_name key
A patch release that stops rejecting valid compose documents which set a
service container_name. The validator was over-strict: a service carrying
container_name: — used to give a service a stable name that other services
connect to — raised unsupported key 'container_name'. Such documents now
convert.
Fix
- The service
container_namekey is accepted.validate()no longer
rejects a service that setscontainer_name:, and the name is made
resolvable to127.0.0.1inside the pod, exactly likehostnameand network
aliases already are: it joins the--add-hostset so other services reach it
by that name. The actual podman container keeps its internal
{pod}-{service}name (used forpodman cp, healthcheck polling, and
diagnostics) — only name resolution is affected, matching the pod's
shared network namespace.
Downstream
No action needed — additive and backward compatible. Documents that previously
failed with unsupported key 'container_name' now emit a pod script; nothing
that converted before changes.
Internals
architecture/supported-subset.mdmerges thehostname/container_name
note explaining both are shared-namespace-resolvable but never change the
real--name.- 92 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.3
compose2pod 0.1.3 — anonymous volumes no longer crash
A patch release fixing a crash on Compose files that use anonymous volumes. A
short-form volume that is a single container path with no source:target (e.g.
- /var/cache/models) passed validation but then crashed the emitter with
ValueError: not enough values to unpack. Such files now convert.
Fix
- Anonymous volumes are emitted as
-v <path>. A colon-less volume entry
is a Compose anonymous volume; it is now rendered verbatim aspodman run -v <path>(podman creates an anonymous volume at that container path) instead of
crashingrun_flags. No host-path translation is applied — the entry names a
container path, not a host source. - Malformed colon-less volumes are rejected loudly. A colon-less entry that
is not absolute (e.g../cache) is invalid Compose and now raises a clear
unsupported-style error instead of silently emitting a broken-v ./cache,
keeping compose2pod's "refuse loudly, never emit a silently broken script"
contract.
Downstream
No action needed — additive and backward compatible. Compose files that
previously crashed with ValueError: not enough values to unpack on an
anonymous volume now emit a pod script; nothing that converted before changes.
Internals
architecture/supported-subset.mddocuments anonymous volumes in the Volumes
matrix (absolute accepted and emitted as-v <path>; non-absolute raises).- 89 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.2
compose2pod 0.1.2 — accept the service hostname key
A patch release that stops rejecting valid compose documents which set a
service hostname. The validator was over-strict: a service carrying
hostname: — used to give a service a stable name that other services connect
to — raised unsupported key 'hostname'. Such documents now convert.
Fix
- The service
hostnamekey is accepted.validate()no longer rejects a
service that setshostname:, and the hostname is made resolvable to
127.0.0.1inside the pod, exactly like a network alias: it joins the
--add-hostset so other services reach the service by that name. Because all
services share one network namespace (and the pod's UTS namespace), only name
resolution is meaningful — no per-container--hostnameis emitted.
Downstream
No action needed — additive and backward compatible. Documents that previously
failed with unsupported key 'hostname' now emit a pod script; nothing that
converted before changes.
Internals
architecture/supported-subset.mdaddshostnameto the supported
service-key matrix with the shared-namespace resolution rationale.- 86 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.1
compose2pod 0.1.1 — accept Compose extension fields
A patch release that stops rejecting valid compose documents which carry
Compose x- extension fields. The validator was over-strict: a top-level
x- block — the idiomatic place to hold a YAML anchor for reuse across
services — raised unsupported top-level keys. Such documents now convert.
Fix
- Compose
x-extension fields are accepted. Per the Compose spec, any
mapping key prefixedx-is user data that tools ignore.validate()now
skipsx--prefixed keys silently at every level it inspects — top level,
service, and healthcheck — instead of raising. The common pattern of holding
shared config in a top-levelx-*anchor block and merging it into services
(&anchor/<<:) works as-is: PyYAML resolves anchors and merge keys at
load time, and the leftover top-levelx-key no longer trips the validator.
Downstream
No action needed — additive and backward compatible. Documents that previously
failed with unsupported top-level keys: ['x-...'] now emit a pod script;
nothing that converted before changes.
Internals
- New
architecture/supported-subset.mdpins the authoritative accept / ignore
/ reject matrix (including thex-rule and the note that abuildsection
is accepted but never built — the--imagevalue is substituted). - 82 tests at 100% line coverage (enforced);
ruff select=ALL,ty, and
eof-fixerclean.
0.1.0
First public release of compose2pod: convert a Docker Compose file into a POSIX sh script that runs its services as a single Podman pod. Extracted from a CI-proven prototype; no prior versions, so nothing to migrate.
Feature
- Compose → single Podman pod.
compose2pod --target <service> --image <ci-image> [file]reads a compose document and emits a shell script that creates one pod (shared network namespace, no bridge), starts dependencies in topological order, gates startup on health by pollingpodman healthcheck run, runs the target service, copies out artifacts, and cleans up via anEXITtrap. - Supported compose subset.
image/build,command,environment/env_file, short-form bindvolumes,healthcheck(CMD / CMD-SHELL),depends_on(service_started/service_healthy/service_completed_successfully), and networkaliases(mapped to--add-host). Anything outside the subset raises a clearUnsupportedComposeError;ports/restart/stdin_open/ttyare ignored with a warning. - CLI and library API. Use the
compose2podcommand (orpython -m compose2pod), or importvalidate,emit_script,EmitOptions, andUnsupportedComposeError. - JSON always, YAML optional. The core reads compose-as-JSON with zero dependencies (
--format json, or pipeyq);pip install compose2pod[yaml]adds direct YAML input.
Why
CI runners often give unprivileged containers a read-only /proc/sys, so netavark can't create bridge networks (upstream declined: podman#20713 closed, netavark#910 unmerged), and there's no systemd to schedule podman healthchecks. docker compose and podman kube play can't work there. A single pod shares one netns (no bridge, no netavark), services reach each other over 127.0.0.1, and health-gated depends_on works via manual podman healthcheck run polling. No existing tool covered that combination.
Packaging
- Published to PyPI via Trusted Publishing:
pip install compose2pod(core, zero runtime dependencies) orpip install compose2pod[yaml](adds PyYAML). requires-python >=3.10,<4; tested on CPython 3.10–3.14. Typed (py.typed). MIT licensed.
Downstream
No action needed — first release, no public API predecessors.
Internals
- 78 tests at 100% line coverage (enforced);
ruff select=ALL,ty, andeof-fixerclean. - One known follow-up is tracked in
planning/deferred.md: hardening the pod-cleanuptrap's nested quoting for the library API path (unreachable from the CLI, which validates pod names).