Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ A supported service key handled *outside* the service-key registry, listed in
`project_dir` (`env_file`, `volumes`), spans keys, or occupies the image/command slot
(`entrypoint`). Structural keys keep their own validate/emit machinery, in the module
that owns the concern. Which side of this line a key falls on is a design ruling, not
a convenience: see [ADR-0008](docs/adr/0008-reject-structural-key-registry.md) and
[ADR-0013](docs/adr/0013-volumes-stays-hand-rolled.md).
a convenience: see
[ADR-0005](docs/adr/0005-structural-keys-and-schema-validators-stay-in-their-owning-modules.md).

**Token**:
The result of rendering one Compose value into a `podman run`/`pod create` argument:
Expand All @@ -53,7 +53,7 @@ document-wide.

**Rule one / rule two**:
The two directions of the Docker-rejection parity rule
([ADR-0009](docs/adr/0009-docker-rejection-parity.md)). "Rule two" is named bare
([ADR-0006](docs/adr/0006-docker-rejection-parity.md)). "Rule two" is named bare
in `parsing.py` comments and in tests, with no restatement at the call site.
**Rule one**: a document `docker compose config` rejects, compose2pod
rejects too — hard, no exceptions. **Rule two**: a document Docker accepts,
Expand Down
2 changes: 1 addition & 1 deletion compose2pod/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def _depends_on_entry_condition(dep: str, spec: dict[str, Any]) -> str:
# defaults to service_started -- that default is Docker's own and
# stays. This one was compose2pod's own invention (`spec.get(...,
# "service_started")`) and was a false green against the hard rule
# in `docs/adr/0009-docker-rejection-parity.md`.
# in `docs/adr/0006-docker-rejection-parity.md`.
msg = f"depends_on entry {dep!r}: missing required key 'condition'"
raise UnsupportedComposeError(msg)
condition = spec["condition"]
Expand Down
2 changes: 1 addition & 1 deletion compose2pod/values.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Value grammars: the shapes `docker compose config` accepts for a scalar key.

compose2pod refuses every document Docker refuses
(`docs/adr/0009-docker-rejection-parity.md`), which means
(`docs/adr/0006-docker-rejection-parity.md`), which means
matching Docker's *value* grammars, not just its types: `mem_limit: ""` and
`cpus: somevalue` are documents Docker will not run.

Expand Down
55 changes: 0 additions & 55 deletions docs/adr/0001-healthcheck-start-period-retries-passthrough.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The healthcheck wait budget is not coupled to `retries`

`run_flags` passes a healthcheck's `start_period` and `retries` through as `--health-start-period`
and `--health-retries`, but the emitted script's `wait_healthy` loop keeps its fixed
`HEALTHY_WAIT_BUDGET_SECONDS` rather than deriving `retries × interval`. `retries` counts the
consecutive failures podman tolerates, not the time until the first check is meaningful, so a
budget derived from it can expire before a long `start_period` has elapsed. The fixed 120 s errs
toward slow starters at the cost of a slower failure signal; a service that needs more makes the
budget configurable, not coupled.
62 changes: 6 additions & 56 deletions docs/adr/0002-zero-dependency-core.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,7 @@
# Zero-dependency core, no compose-parser dependency
# Zero-dependency core

**Decision:** The `compose2pod` core package has zero runtime dependencies
(stdlib only). PyYAML is shipped only behind the optional `[yaml]` extra.
The core does not depend on any compose-spec parser library.

## Context

`compose2pod` reads a Docker Compose document and must decide how it is
loaded (JSON/YAML) and validated against the supported subset. Two
dependency questions came up while designing the package:

1. Should YAML parsing be a hard dependency, so the CLI always accepts
`docker-compose.yml` directly?
2. Should the core adopt an existing compose-spec parser library
(`compose-spec`, `compose-pydantic`) instead of hand-rolled subset
validation, to get broader spec coverage for free?

The primary differentiator for `compose2pod` is that it installs with no
compiled wheels and runs in minimal Python images (the same CI containers
that motivate the tool in the first place — see the design's "Why this
exists"). Any hard dependency, especially one with a compiled wheel,
undermines that.

## Decision & rationale

- **YAML stays optional.** JSON parsing via stdlib `json` is always
available. YAML parsing via PyYAML is the optional `[yaml]` extra; without
it, `--format yaml` errors with an actionable message pointing at `pip
install compose2pod[yaml]` or piping through `yq -o=json`. This keeps the
dependency-constrained-CI path (where even PyYAML may not be installable)
fully functional.
- **No compose-spec parser dependency.** Researched candidates
(`compose-spec`, `compose-pydantic`) both require pydantic v2 (a compiled
`pydantic-core` wheel) and are early-stage single-maintainer 0.x projects.
Adopting one would:
(a) break the zero-dependency differentiator for the core,
(b) not actually remove the subset validation — full-spec parsers
permissively accept constructs (`configs`, `secrets`, `profiles`,
`deploy`, long-form volumes, ...) that `compose2pod` cannot turn into a
single pod, so a subset gate is still required regardless of what parses
the document, and
(c) add supply-chain risk (compiled wheel, single maintainer, early 0.x)
to a package whose whole pitch is minimal-footprint installability.
- A future optional `[strict]` extra could cross-validate against
`compose-spec` for users who want full-spec checking, but that is
deliberately out of scope for v1 (YAGNI) — no user need has been
identified yet.

**Revisit trigger:** any of —

- A mature, pure-Python (no compiled wheel) compose-spec parser reaches a
stable 1.x release with more than one maintainer, and a concrete user
need for full-spec validation (beyond the subset `compose2pod` supports)
emerges.
- PyYAML itself becomes uninstallable in a target CI environment that
`compose2pod` needs to support, forcing a rethink of the YAML story.
The core has no runtime dependencies: JSON via the stdlib always works and PyYAML sits behind the
`[yaml]` extra, because the tool exists for minimal CI images where even a compiled wheel may not
install. No compose-spec parser library is used either. The candidates require `pydantic-core`,
are early single-maintainer projects, and would not remove the subset gate anyway, since a
full-spec parser accepts constructs a single pod cannot express.
52 changes: 0 additions & 52 deletions docs/adr/0003-reject-namespace-network-keys.md

This file was deleted.

12 changes: 12 additions & 0 deletions docs/adr/0003-the-shared-namespace-decides-key-classification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The shared namespace decides which keys are refused, inert, or pod-level

Every service runs in one pod sharing `net`, `uts`, `ipc` and `cgroup`, and the shared network
namespace, with localhost discovery through per-container `--add-host`, is the reason the tool
exists. Keys that pull a container out of it (`network_mode`, `links`, `external_links`,
`expose`) are refused permanently; per-container namespace overrides (`ipc`, `uts`, `domainname`,
`cgroup`, `userns_mode`) are refused while the pod keeps its default `--share`. `dns*` and
`sysctls` are pod-level, unioned and conflict-checked across the closure onto
`podman pod create`, because a container that joined the pod owns neither namespace and podman
rejects the per-container flag. `stop_signal` and `stop_grace_period` are accepted but inert, in
`IGNORED_SERVICE_KEYS` with a warning: the script tears down with `pod rm -f` and never runs
`podman stop`, so the flags would set metadata nothing consults.
40 changes: 0 additions & 40 deletions docs/adr/0004-stop-lifecycle-keys-inert.md

This file was deleted.

11 changes: 11 additions & 0 deletions docs/adr/0004-validate-and-emit-both-read-the-raw-dict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `validate` and `emit` both read the raw dict

`validate(compose)` and `emit_script(compose, options)` both take the raw compose dict. There is
no typed `CheckedDocument` produced by one and consumed by the other, and the graph queries in
`graph.py` (`depends_on`, `hostnames`, `startup_order`) normalise and raise on bad shape wherever
they are called rather than being computed once and threaded through. The gate is
target-agnostic and emit is target-scoped, so one shared model cannot span them, and the typed
model would cost a registry rewrite plus a breaking API for a gap that is closed differently:
`emit._plan`, the single traversal both `emit_script` and `referenced_variables` project from,
calls `validate(compose)` itself, so a library caller cannot reach a raw `KeyError` or a corrupted
flag. A second emit consumer is what would make the typed model pay.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Structural keys and schema validators stay in their owning modules

`SERVICE_KEYS` holds the keys that share one `emit(value) -> list[Token]` interface. The
structural keys in `STRUCTURAL_KEYS` stay in the module that owns their concern (`emit.py`,
`graph.py`, `pod.py`, `stores.py`, `resources.py`, `healthcheck.py`) because they have at least
six emit shapes: `entrypoint` emits on both sides of the image token and its string form cancels
`command`, `volumes` and `env_file` need `project_dir`, `depends_on` drives ordering, `dns` and
`sysctls` aggregate onto the pod, `secrets` and `deploy` need document scope. A registry over
them would be `Any`-typed callbacks, a false seam, and widening `KeySpec.emit` to carry a context
for the two keys that need it would tax the ~29 that do not, which is why `tmpfs` joined the
registry and `volumes` did not. The same holds for the ~14 strict-schema validators: the shareable
core is a three-line unknown-key check and everything around it differs per site (`x-` policy,
pre-check shape, required keys, message text), so only the two identical definition validators
share `parsing._validate_top_level_definition`. Reopened 2026-07-15 with two reviewers blind to
this record arguing opposite sides; both converged on it. A third reader of the key-name list, or
a cluster of new keys sharing one new shape, earns a narrow sub-registry for that shape, never a
universal one.
Loading
Loading