Skip to content

0.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 11:43
· 10 commits to main since this release
b80f0b6

compose2pod 0.3.0 — measured parity with Docker, and a much wider accepted subset

This release makes the subset's boundary a measured one. compose2pod now
refuses every document docker compose config refuses — verified continuously
by a differential conformance harness that runs both the real Docker CLI and the
real compose2pod pipeline over the same YAML and asserts the rule. On top of that
hard floor, the accepted subset grew substantially: quoted booleans, env_file
and volumes long-form (with --mount), the full Compose duration grammar, and
the volume mount option maps. Where compose2pod still refuses a form Docker
accepts, that refusal is now measured, documented, and — for the permanent ones —
recorded as a decision with the exact Podman limitation behind it.

The parity thesis

The governing rule (decisions/2026-07-14-docker-rejection-parity.md):
accepted(compose2pod) ⊆ accepted(docker) — compose2pod must reject every
document Docker rejects, so a file that compiles is a file Docker would run.

  • Reject every document docker compose config refuses. The value grammars
    (size, number, integer, count, duration, port) now match Docker's
    own — mem_limit: "", cpus: somevalue, a unitless healthcheck duration, an
    out-of-range port, a whitespace-padded scalar, all refused where Docker refuses
    them, instead of being silently accepted and compiled into a script Docker
    itself would not run.
  • Read YAML the way Docker reads it (1.2). A bare on/off/yes/no stays
    an ordinary string (not a boolean), and a bare 1e3 is the float 1000.0
    matching Docker's YAML-1.2 parser, so SSL: on reaches the container as
    SSL=on and an on: key no longer resolves to True.
  • Match Docker's null-value policy. A bare key: (null) is refused exactly
    where Docker refuses one, and treated as "unset" exactly where Docker does.
  • Close the structural-key and reference gates. Non-string mapping keys,
    malformed nested shapes, undefined named-volume/network references, and
    strict long-form schemas (build, depends_on, networks, volumes,
    secrets/configs refs) are validated to Docker's own grammar.
  • A differential conformance harness (tests/conformance/, CI-only) probes
    every registry key × every hostile shape plus a hand-authored corpus, running
    Docker and compose2pod over identical YAML. A document Docker rejects that
    compose2pod accepts fails the build; the reverse (an over-rejection) is
    reported and catalogued, never hidden.

Feature — a wider accepted subset

  • Quoted booleans on every boolean field. read_only: "yes", tty: "true",
    init: "on", and the same on build, network/volume definitions, and
    depends_on — the YAML-1.1 boolean spellings Docker casts a string field
    through — are now accepted, via one shared values.is_bool_like/as_bool
    seam, coercing before emit so a quoted "false" never leaks a set flag.
  • env_file long-form. env_file: [{path, required, format}] is accepted;
    required: false is honored with a run-time [ -f path ] guard so an absent
    optional file is skipped, format: raw is accepted, and path resolves as
    before.
  • volumes long-form (--mount). The mapping entry
    {type, source, target, read_only, consistency} for type in
    bind/volume/tmpfs compiles to podman run --mount, and the nested option
    maps — bind: {propagation, selinux}, volume: {subpath},
    tmpfs: {size, mode} — map to the corresponding --mount options (selinux
    z/Zrelabel=shared/private).
  • The full Compose duration grammar for healthcheck interval. Compound and
    larger units — 1h, 1h30m, 1d, 1w, 1.5d, -1h — are accepted (the
    interval paces the polling loop; Podman never sees it), overflow- and
    whitespace-safe.
  • Podman-version guard. Generated scripts warn at run time when Podman is
    below the version with the /etc/hosts pod-wide fix, so a silent name-
    resolution failure surfaces as an explicit warning.

Fix

  • --add-host scoped to the target's dependency closure — a host entry for a
    service outside the run set no longer lands on the pod-create line.
  • Two hard-rule false greens, caught by the differential harness's own
    adversarial review and closed:
    a trailing newline slipping past a $-anchored
    value grammar (now \Z, reachable via a YAML block scalar), and a padded /
    negative / float value on the tmpfs mount sub-schema (Docker validates it as
    unsigned; Podman's crun rejects a float mode).

Internals

  • Registry unification. environment and tmpfs moved into the
    SERVICE_KEYS registry (one validate + emit + merge spec per key); the compose
    reader (the YAML-1.2 SafeLoader and format dispatch) was extracted into
    read.py with its own test surface. volumes was deliberately left
    hand-rolled — recorded as a decision (its emit needs project_dir, its
    references are document-level).
  • A decisions log now records the rulings that shape the boundary — the
    docker-rejection-parity rule, the list-of-str (sysctls/volumes: ["a"])
    legitimate refusals, the volumes-stays-hand-rolled call, and the
    measured-negative-numerics finding — each with the exact Docker/Podman behavior
    and a revisit trigger, so none is re-litigated.
  • Tests at 100% line coverage (enforced); ruff select=ALL, ty, eof-fixer
    clean; the CI-only integration harness (real Podman) and conformance harness
    (real Docker CLI) both green.

Why

0.2.0 rounded out the common compose keys. This release answers a sharper
question — which documents does compose2pod's "yes" actually mean? — with a
measured answer: exactly the ones Docker would run, minus a small, catalogued set
of forms Podman genuinely cannot express. The differential harness makes that a
property the build enforces, not a claim in the README, and it repeatedly caught
false greens that unit tests (which assert on generated text) could not.

Downstream

  • Stricter where Docker is strict. A document that previously compiled but
    that docker compose config rejects — a native number on a duration field, a
    malformed size, a null where Docker refuses one, a bare on/yes you relied on
    being a boolean — now raises UnsupportedComposeError. This is the point: the
    script you get is one Docker would run.
  • Wider where Podman can express it. Quoted booleans, env_file/volumes
    long-form, and compound durations mean fewer documents need pre-editing.
  • The honest boundary. Three forms Docker accepts stay refused, each measured
    and documented: the image mount type and a Windows drive-letter source
    (planning/deferred.md, genuine parser gaps), and sysctls/volumes: ["a"]
    (decisions/, permanent — Podman cannot form the flag).