Skip to content

Releases: modern-python/compose2pod

0.2.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 11:05
1fcb527

compose2pod 0.2.0 — most of the common compose subset, and a real-podman test harness

This release rounds out the per-container and pod-level keys most compose
files actually use — extends, secrets/configs, resource limits,
ulimits, and pod-wide dns/sysctls/extra_hosts — and adds a CI-only
integration harness that runs generated scripts against real Podman. That
harness caught a real bug on its first run, fixed in this same release: see
Fix, below.

Fix

  • --add-host moved from podman run to podman pod create. Current
    Podman rejects --add-host on a container joining a pod ("network cannot
    be configured when it is shared with a pod") — so any generated script for
    a pod with more than one named service failed at run time. All
    --add-host emission (service-name/hostname/container_name/network
    aliases, and extra_hosts) now lands on the single podman pod create
    line instead, alongside dns/sysctls. extra_hosts is consequently now
    genuinely pod-wide (previously per-service); a host name landing on two
    different addresses — across services' extra_hosts, or against an
    alias's fixed 127.0.0.1 — is refused (UnsupportedComposeError) rather
    than resolved by guessing, matching the existing sysctls conflict rule.
  • validate()/emit_script() reject malformed input cleanly instead of
    crashing.
    A malformed depends_on, healthcheck, hostname/
    container_name, tmpfs, or per-service networks shape now raises
    UnsupportedComposeError at the validation/emit boundary instead of an
    uncaught AttributeError/ValueError deeper in the pipeline.
  • emit_script validates the pod name itself, not only the CLI — an
    adversarial EmitOptions.pod (quotes, spaces, $(...)) now raises
    cleanly instead of producing a malformed shell trap.

Feature

  • extends. Same-file Compose extends: {service: <name>} is resolved
    before validation (transitive, cycle-checked, per-key merge — concat for
    list keys like cap_add/volumes, local-wins merge for environment/
    labels/healthcheck, override for scalars and command/entrypoint).
    Cross-file extends: {file: ...} stays refused, keeping the
    single-document input model.
  • secrets and configs. Top-level secrets:/configs: (file:,
    environment:, and — configs only — inline content: sources) compile to
    podman secret create, mounted via --secret at /run/secrets/<name>
    (secrets) or the container root /<name> (configs, absolute target:
    only). external: true is refused; a secret and same-named config don't
    collide (distinct store names).
  • Resource limits. Both the legacy scalar keys (mem_limit, cpus,
    pids_limit, cpu_shares, cpuset, shm_size, oom_score_adj,
    oom_kill_disable, ...) and the modern deploy.resources.limits/
    .reservations block map onto --memory/--cpus/--pids-limit/
    --memory-reservation. A legacy key and its deploy.resources counterpart
    targeting the same flag is refused rather than picking an undefined
    precedence.
  • ulimits. A mapping of limit name to a scalar (nproc: 65535) or a
    {soft, hard} pair, emitted as --ulimit.
  • Pod-level dns/sysctls/extra_hosts. These apply to every
    container in the pod (one shared /etc/resolv.conf, sysctl set, and
    /etc/hosts) — dns/dns_search/dns_opt are unioned across the
    target's dependency closure; sysctls union by key, refusing a same-key
    conflict; extra_hosts merges the same way (see Fix, above). validate()
    warns whenever any service in the document declares one of these, even
    outside the target's closure.
  • Core process, confinement, and metadata keys: entrypoint, user,
    working_dir, group_add, labels, read_only, init, privileged,
    cap_add, cap_drop, security_opt, platform, devices,
    annotations, pull_policy. Each is an honest, validated passthrough to
    its podman run flag.
  • profiles and stop_signal/stop_grace_period are now accepted and
    ignored (with a warning) rather than rejected — compose2pod's run set is
    always --target plus its depends_on closure, and the generated script
    always force-removes the pod, so neither key changes anything it can act
    on. A target depends_on reaching a service outside its declared profile
    still runs it (the closure is authoritative — more permissive than
    Compose, never a silent drop).

Packaging

  • resolve_extends is exported from compose2pod.__all__, alongside
    the existing EmitOptions, UnsupportedComposeError, emit_script,
    to_shell, and validate.

Why

Prior releases built the core single-pod model and its interpolation
semantics; this one rounds out the compose keys real-world compose files
actually use, so fewer documents need pre-editing before they run.
Discovering the --add-host regression only on a real Podman run — not from
any of the 363 existing unit tests, all of which assert on generated
script text — is exactly why the integration harness (see Internals) was
worth building: some regressions are only visible to the runtime it targets.

Downstream

  • Compose files using extra_hosts for a service-specific override: it
    is now pod-wide. A host name your extra_hosts maps to one address must
    not conflict with another service's mapping, or with an alias's fixed
    127.0.0.1 — compose2pod now refuses rather than silently picking one.
  • Any pod that previously failed with network cannot be configured when it is shared with a pod now works without changes to your compose file.
  • API consumers: compose2pod.resolve_extends(compose) is now
    available for flattening extends outside the CLI pipeline.

Internals

  • 363 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.
  • A new CI-only integration harness (tests/integration/, 10 scenarios)
    renders real compose documents and runs the generated script against real
    Podman on ubuntu-latest, asserting on exit code and output — it caught
    the --add-host regression above on its first run. Kept out of the
    100%-coverage gate and the fast suite via a location-based marker.
  • compose2pod/keys.py's SERVICE_KEYS registry unifies what was
    previously three hand-synced tables (validation, emission, and the
    supported-key set) into one (validate, emit) pair per key.

0.1.8

Choose a tag to compare

@github-actions github-actions released this 09 Jul 05:08
ca24f96

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() (new compose2pod/shell.py) re-encodes every compose-derived
    value (environment, image, command, volumes, tmpfs, env_file,
    healthcheck test) 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 POSIX sh
    parameter expansion. A bare $VAR/${VAR} is emitted as ${VAR-} so an
    unset variable expands to empty under the script's set -eu instead 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

  • environment null value is host passthrough. A null mapping value
    (KEY:) now emits a bare -e KEY — passing KEY through 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
    UnsupportedComposeError instead of silently dropping the trailing text.

Packaging

  • Public API: the generation-time interpolate function is removed;
    to_shell is 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.interpolate is gone. Use
    compose2pod.to_shell(value), which emits a runtime-expandable shell fragment
    rather than resolving the value in-process.

Internals

  • architecture/supported-subset.md rewritten to describe run-time expansion,
    the ${VAR-}/$$/${VAR:?} semantics, env_file interpolation, the
    reference note, null-value passthrough, and malformed-reference rejection.
  • 123 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean. test_shell.py executes emitted fragments under real
    sh -euc to prove run-time expansion, the ${VAR:?} abort, and
    command-substitution inertness.

0.1.7

Choose a tag to compare

@github-actions github-actions released this 08 Jul 19:57
869ba8b

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 against os.environ before 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.md documents the supported interpolation
    forms and the explicit non-goal: no .env file loading, only the caller's
    existing environment is consulted.
  • 123 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.6

Choose a tag to compare

@github-actions github-actions released this 08 Jul 19:36
392c22f

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 tmpfs key is accepted. validate() no longer rejects a
    service that sets tmpfs:. Each entry is emitted 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.

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.md documents tmpfs in the Service keys
    matrix.
  • 98 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.5

Choose a tag to compare

@github-actions github-actions released this 08 Jul 15:16
744a484

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 plain podman run -v — so no
    explicit podman volume create step is needed. The volume persists on the
    host after the pod is removed, identical to docker compose down without
    -v.
  • Top-level volumes: is accepted (ignored). Mirrors the existing
    top-level networks treatment: the block's driver/driver_opts and
    external settings are never read, since implicit creation with default
    options is what podman does regardless. A non-default driver or an
    external: true volume 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.md documents 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-fixer clean.

0.1.4

Choose a tag to compare

@github-actions github-actions released this 08 Jul 14:56
a4d1f81

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_name key is accepted. validate() no longer
    rejects a service that sets container_name:, and the name is made
    resolvable to 127.0.0.1 inside the pod, exactly like hostname and network
    aliases already are: it joins the --add-host set so other services reach it
    by that name. The actual podman container keeps its internal
    {pod}-{service} name (used for podman 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.md merges the hostname/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-fixer clean.

0.1.3

Choose a tag to compare

@github-actions github-actions released this 08 Jul 14:41
5bed4db

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 as podman run -v <path> (podman creates an anonymous volume at that container path) instead of
    crashing run_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.md documents 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-fixer clean.

0.1.2

Choose a tag to compare

@github-actions github-actions released this 08 Jul 14:16
7e7f733

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 hostname key is accepted. validate() no longer rejects a
    service that sets hostname:, and the hostname is made resolvable to
    127.0.0.1 inside the pod, exactly like a network alias: it joins the
    --add-host set 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 --hostname is 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.md adds hostname to the supported
    service-key matrix with the shared-namespace resolution rationale.
  • 86 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.1

Choose a tag to compare

@github-actions github-actions released this 04 Jul 10:46
2f95b59

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 prefixed x- is user data that tools ignore. validate() now
    skips x--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-level x-* anchor block and merging it into services
    (&anchor / <<:) works as-is: PyYAML resolves anchors and merge keys at
    load time, and the leftover top-level x- 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.md pins the authoritative accept / ignore
    / reject matrix (including the x- rule and the note that a build section
    is accepted but never built — the --image value is substituted).
  • 82 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.0

Choose a tag to compare

@github-actions github-actions released this 03 Jul 20:03
1f89129

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 polling podman healthcheck run, runs the target service, copies out artifacts, and cleans up via an EXIT trap.
  • Supported compose subset. image/build, command, environment/env_file, short-form bind volumes, healthcheck (CMD / CMD-SHELL), depends_on (service_started / service_healthy / service_completed_successfully), and network aliases (mapped to --add-host). Anything outside the subset raises a clear UnsupportedComposeError; ports/restart/stdin_open/tty are ignored with a warning.
  • CLI and library API. Use the compose2pod command (or python -m compose2pod), or import validate, emit_script, EmitOptions, and UnsupportedComposeError.
  • JSON always, YAML optional. The core reads compose-as-JSON with zero dependencies (--format json, or pipe yq); 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) or pip 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, and eof-fixer clean.
  • One known follow-up is tracked in planning/deferred.md: hardening the pod-cleanup trap's nested quoting for the library API path (unreachable from the CLI, which validates pod names).