Skip to content

Commit 46776c2

Browse files
authored
docs: correct README facts and document CLI options (#148)
- Move profiles out of supported composition into an accepted-and-ignored list - Quote 'compose2pod[yaml]' in README and the missing-extra error so zsh accepts it - Add help text for --pod-name and --format and document every CLI option - Plain bullets, no dashes, shorter Requirements and conformance prose
1 parent 844176f commit 46776c2

4 files changed

Lines changed: 55 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,33 @@ Convert a Docker Compose file into a POSIX `sh` script that runs its services as
2222

2323
Built for CI and test environments where you can't use `docker compose` or `podman kube play`:
2424

25-
- **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via a generated `/etc/hosts` the script owns.
26-
- **No systemd.** Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling `podman healthcheck run` directly, so `depends_on: service_healthy` works without systemd.
27-
- **No heavy runtime.** The core is stdlib-only — no dependencies, no compiled wheels — so it installs and runs in minimal Python images.
25+
- No bridge networking or netavark. Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via a generated `/etc/hosts` the script owns.
26+
- No systemd. Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling `podman healthcheck run` directly, so `depends_on: service_healthy` works without systemd.
27+
- No heavy runtime. The core is stdlib-only, with no dependencies and no compiled wheels, so it installs and runs in minimal Python images.
2828

2929
## Requirements
3030

31-
**Podman 4.9 or newer.** Every form compose2pod accepts is one Podman expresses across that
32-
whole range, from the floor to the newest release measured (6.1): a mount option a later
33-
Podman adds is refused until the floor reaches it, so a document that compiles here runs on
34-
any supported Podman rather than only the newest one
31+
Podman 4.9 or newer. compose2pod accepts only forms that every Podman from 4.9 to 6.1 can
32+
run, so a script it generates runs on any of them
3533
([ADR-0006](https://github.com/modern-python/compose2pod/blob/main/docs/adr/0006-docker-rejection-parity.md)).
3634

3735
compose2pod's generated scripts own `/etc/hosts`: they write it to a temp
3836
file and bind-mount it read-only into every container under `--no-hosts`, so
3937
pod-internal name resolution works on any Podman version. `host.containers.internal` /
40-
`host.docker.internal` are not provided — add an explicit `extra_hosts`
38+
`host.docker.internal` are not provided; add an explicit `extra_hosts`
4139
entry if you need them.
4240

4341
## Install
4442

4543
```bash
4644
pip install compose2pod # core: reads compose as JSON
47-
pip install compose2pod[yaml] # optional: read YAML directly (adds PyYAML)
45+
pip install 'compose2pod[yaml]' # optional: read YAML directly (adds PyYAML)
4846
```
4947

5048
## Usage
5149

5250
```bash
53-
# YAML directly (needs the [yaml] extra)
51+
# YAML directly (needs the 'compose2pod[yaml]' extra)
5452
compose2pod docker-compose.yml --target app --image myimage:ci > run.sh
5553

5654
# Or stay dependency-free by piping JSON (e.g. via yq)
@@ -59,37 +57,51 @@ yq -o=json '.' docker-compose.yml | compose2pod --target app --image myimage:ci
5957
sh ./run.sh
6058
```
6159

60+
Options:
61+
62+
- `--target`: the service to run in the foreground (required).
63+
- `--image`: the CI image that replaces every service with a `build` section (required).
64+
- `--command`: a shell command overriding the target service's command.
65+
- `--project-dir`: the host path that relative volume and `env_file` sources resolve against (default `.`).
66+
- `--pod-name`: the name of the Podman pod, also used as the prefix of every container name (default `test-pod`).
67+
- `--format`: the input format, one of `auto`, `json`, `yaml` (default `auto`, which tries JSON, then YAML).
68+
- `--artifact SRC:DST`: a file to `podman cp` out of the target container after it exits (repeatable).
69+
- `--allow-exit-code`: a target exit code treated as success in addition to 0 (repeatable).
70+
6271
## Supported compose subset
6372

64-
compose2pod refuses **every document `docker compose config` refuses** — a
65-
measured property, checked continuously by a differential conformance harness
66-
that runs the real Docker CLI and the real compose2pod pipeline over the same
67-
YAML. So a file that compiles is a file Docker would run; where compose2pod
68-
still refuses a form Docker accepts, it is because Podman genuinely cannot
69-
express it (each such case is documented, not guessed).
73+
compose2pod refuses every document `docker compose config` refuses. A test
74+
harness runs `docker compose config` and compose2pod over the same files to
75+
check this. So a file that compiles is a file Docker would run; where compose2pod
76+
still refuses a form Docker accepts, it is because Podman cannot express it;
77+
each case is documented in [`docs/adr/`](https://github.com/modern-python/compose2pod/tree/main/docs/adr/).
7078

7179
Within that boundary it covers most of what real compose files use:
7280

73-
- **Services** — `image`/`build`, `command`/`entrypoint`, `environment` and
81+
- Services: `image`/`build`, `command`/`entrypoint`, `environment` and
7482
`env_file` (string and long-form `{path, required, format}`), `volumes`
7583
(short-form and long-form `--mount`, including the `bind` and `tmpfs`
7684
option maps), `tmpfs`, `healthcheck`, `depends_on` (all conditions), `links`
7785
(read as a dependency plus a hostname alias, as Docker reads it), network
7886
`aliases`, `hostname`/`container_name`.
79-
- **Confinement & metadata** — `user`, `working_dir`, `read_only`, `init`,
87+
- Confinement and metadata: `user`, `working_dir`, `read_only`, `init`,
8088
`privileged`, `cap_add`/`cap_drop`, `security_opt`, `devices`, `group_add`,
8189
`platform`, `labels`, `annotations`, `pull_policy` (the quoted-boolean and
8290
YAML-1.1 spellings Docker accepts, too).
83-
- **Resources** — the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
91+
- Resources: the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
8492
`ulimits`, …) and the modern `deploy.resources` block.
85-
- **Pod-wide** — `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
86-
- **Composition** — same-file `extends`, `secrets`/`configs`, `profiles`.
93+
- Pod-wide: `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
94+
- Composition: same-file `extends`, `secrets`/`configs`.
95+
96+
Accepted and ignored with a warning, since they mean nothing inside one shared pod:
97+
`ports`, `expose`, `restart`, `stdin_open`, `tty`, `stop_signal`,
98+
`stop_grace_period`, and `profiles` (every service runs regardless of profile).
8799

88100
Compose extension fields (any `x-`-prefixed key) and YAML anchors are accepted
89-
as-is, so a top-level `x-*` anchor block for shared config just works.
101+
as-is, so a top-level `x-*` anchor block for shared config is accepted.
90102
`${VAR}`-style variable interpolation is left live in the generated script,
91103
resolved by its shell against the environment present when the script runs (no
92-
`.env` file support). The boundary rulings — which forms are refused, and why —
104+
`.env` file support). The boundary rulings, which forms are refused and why,
93105
are recorded in [`docs/adr/`](https://github.com/modern-python/compose2pod/tree/main/docs/adr/).
94106

95107
## Status

‎compose2pod/cli.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ def main(argv: list[str] | None = None) -> int:
2121
parser.add_argument("--image", required=True, help="CI image replacing services that have a build section")
2222
parser.add_argument("--project-dir", default=".", help="host path relative volume/env_file sources resolve to")
2323
parser.add_argument("--command", default="", help="shell command overriding the target service command")
24-
parser.add_argument("--pod-name", default="test-pod")
25-
parser.add_argument("--format", choices=("auto", "json", "yaml"), default="auto")
24+
parser.add_argument(
25+
"--pod-name",
26+
default="test-pod",
27+
help="name of the podman pod, also the container name prefix (default: test-pod)",
28+
)
29+
parser.add_argument(
30+
"--format",
31+
choices=("auto", "json", "yaml"),
32+
default="auto",
33+
help="input format; auto tries JSON, then YAML (default: auto)",
34+
)
2635
parser.add_argument(
2736
"--artifact",
2837
action="append",

‎compose2pod/read.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class Loader(yaml_module.SafeLoader):
7070

7171
def _load_yaml(text: str) -> Any: # noqa: ANN401 - returns arbitrary parsed compose data
7272
if _yaml is None:
73-
msg = "YAML input requires the 'yaml' extra: pip install compose2pod[yaml] (or pipe JSON via yq)"
73+
msg = "YAML input requires the 'yaml' extra: pip install 'compose2pod[yaml]' (or pipe JSON via yq)"
7474
raise UnsupportedComposeError(msg)
7575
try:
7676
return _yaml.load(text, Loader=_build_yaml_loader(_yaml)) # noqa: S506 - SafeLoader subclass, not full load

‎tests/test_cli.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ def test_yaml_without_pyyaml_errors(
9696
monkeypatch.setattr(read_module, "_yaml", None)
9797
rc = run_main("services: {}", ["--target", "app", "--image", "i", "--format", "yaml"], monkeypatch)
9898
assert rc == EXIT_USAGE_ERROR
99-
assert "requires the 'yaml' extra" in capsys.readouterr().err
99+
assert "pip install 'compose2pod[yaml]'" in capsys.readouterr().err
100+
101+
def test_help_describes_pod_name_and_format(self, capsys: pytest.CaptureFixture[str]) -> None:
102+
with pytest.raises(SystemExit):
103+
main(["--help"])
104+
help_text = " ".join(capsys.readouterr().out.split())
105+
assert "--pod-name POD_NAME name of the podman pod" in help_text
106+
assert "--format {auto,json,yaml} input format" in help_text
100107

101108
def test_invalid_yaml_returns_2(self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
102109
rc = run_main("a: [1, 2", ["--target", "app", "--image", "i", "--format", "yaml"], monkeypatch)

0 commit comments

Comments
 (0)