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
22 changes: 13 additions & 9 deletions architecture/supported-subset.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ warns (ignored, behavior-neutral inside a single pod) or raises
## Service keys

- **Supported:** `image`, `build`, `command`, `environment`, `env_file`,
`volumes`, `healthcheck`, `depends_on`, `networks`, `hostname`. compose2pod
never builds: a `build` section is accepted but its contents (context,
dockerfile, args) are not read — `image_for` (`compose2pod/emit.py`) runs
the CI image supplied via `--image` for any service that has one.
- **`hostname`:** the service's hostname is made resolvable to `127.0.0.1`
like a network alias (added to the shared `--add-host` set), so other
services can reach it by that name. The pod shares the UTS namespace, so a
service's own hostname is the pod's; only name resolution is meaningful,
and no per-container `--hostname` is emitted.
`volumes`, `healthcheck`, `depends_on`, `networks`, `hostname`,
`container_name`. compose2pod never builds: a `build` section is accepted
but its contents (context, dockerfile, args) are not read — `image_for`
(`compose2pod/emit.py`) runs the CI image supplied via `--image` for any
service that has one.
- **`hostname` and `container_name`:** both are made resolvable to
`127.0.0.1` like a network alias (added to the shared `--add-host` set), so
other services can reach the service by either name. The pod shares the UTS
namespace, so a service's own hostname is the pod's, and the actual podman
container is always named `{pod}-{service}` regardless of `container_name`
(used internally for `podman cp`, healthcheck polling, and target-container
diagnostics) — only name *resolution* is meaningful to other services, and
no per-container `--hostname` or renamed `--name` is emitted.
- **Ignored (warns):** `ports`, `restart`, `stdin_open`, `tty` — meaningless
or irrelevant inside a single shared-namespace pod.
- **Extension fields:** any `x-`-prefixed service key is accepted and ignored
Expand Down
5 changes: 4 additions & 1 deletion compose2pod/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ def depends_on(svc: dict[str, Any]) -> dict[str, str]:


def hostnames(services: dict[str, Any]) -> list[str]:
"""All names other services may use to reach a service: names, hostnames, then aliases."""
"""All names other services may use to reach a service: names, hostnames/container names, then aliases."""
names = list(services)
for svc in services.values():
hostname = svc.get("hostname")
if hostname:
names.append(hostname)
container_name = svc.get("container_name")
if container_name:
names.append(container_name)
networks = svc.get("networks")
if isinstance(networks, dict):
for network in networks.values():
Expand Down
1 change: 1 addition & 0 deletions compose2pod/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"depends_on",
"networks",
"hostname",
"container_name",
}
IGNORED_SERVICE_KEYS = {"ports", "restart", "stdin_open", "tty"}
SUPPORTED_HEALTHCHECK_KEYS = {"test", "interval", "timeout", "retries", "start_period"}
Expand Down
58 changes: 58 additions & 0 deletions planning/changes/2026-07-08.03-container-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
summary: Accept the service `container_name` key, resolving it to 127.0.0.1 like `hostname` and network aliases.
---

# Change: Support the service `container_name` key

**Lane:** lightweight — two files (`parsing.py`, `graph.py`), no new file, no
public-API change, mirrors the already-shipped `hostname` support exactly.

## Goal

`validate()` rejects the service `container_name` key
(`service 'application': unsupported key 'container_name'`). A real compose
file sets `container_name: calutron-ronline` and other services (or the host
tooling) reach it by that name — the reporting user confirmed it is "also used
as hostname." Fix: accept `container_name` and make it resolvable to loopback,
the same treatment `hostname` already gets.

## Approach

Same shape as the `hostname` change
(`planning/changes/2026-07-08.01-service-hostname.md`):

- `parsing.py`: add `"container_name"` to `SUPPORTED_SERVICE_KEYS`.
- `graph.py`: `hostnames()` also collects `svc.get("container_name")`
(truthy-guarded, same pattern as `hostname`), so it becomes an
`--add-host <container_name>:127.0.0.1` entry.

Explicitly **not** changed: `emit.run_flags` keeps naming the actual podman
container `{pod}-{name}` (`emit.py:47`) — that internal name is what
`podman cp`, the healthcheck `wait_healthy` calls, and the target-container
inspect/log calls all reference. Renaming the real `--name` to the compose
`container_name` would require rewiring every one of those references for no
resolution benefit: the pod's shared UTS/network namespace means only name
*resolution* is meaningful to other services, not the literal running
container's identity, which `--add-host` already supplies.

## Non-goals

- No `podman run --name <container_name>` — see rationale above.
- No uniqueness/collision validation between `container_name`, `hostname`, and
service names — `hostnames()` already tolerates duplicate entries (harmless
repeated `--add-host` of the same loopback mapping), consistent with the
`hostname` change's accepted risk.

## Testing

TDD, `just test-ci` at 100% line coverage.

- Unit (`test_parsing.py`): a service with `container_name` validates without raising.
- Unit (`test_graph.py`): `hostnames()` includes a service's `container_name`.
- Integration (`test_emit.py`): `container_name` round-trips through
`validate` → `emit_script` and the rendered script contains
`--add-host <name>:127.0.0.1`.

## Risk

Low — identical mechanism to the already-shipped, tested `hostname` support.
19 changes: 19 additions & 0 deletions tests/test_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,25 @@ def test_hostname_becomes_add_host_entry(self) -> None:
script = emit_script(compose=compose, options=options)
assert "--add-host keydb-test-server-0:127.0.0.1" in script

def test_container_name_becomes_add_host_entry(self) -> None:
compose = {
"services": {
"application": {"image": "app", "container_name": "calutron-ronline"},
}
}
options = EmitOptions(
target="application",
ci_image="ci",
command="",
pod="testpod",
project_dir="/proj",
artifacts=[],
allow_exit_codes=[],
)
assert validate(compose) == []
script = emit_script(compose=compose, options=options)
assert "--add-host calutron-ronline:127.0.0.1" in script

def test_target_without_command_uses_service_command(self, chats_compose: dict) -> None:
options = EmitOptions(
target="application",
Expand Down
4 changes: 4 additions & 0 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def test_collects_service_hostname(self) -> None:
services = {"keydb": {"image": "x", "hostname": "keydb-test-server-0"}}
assert hostnames(services) == ["keydb", "keydb-test-server-0"]

def test_collects_service_container_name(self) -> None:
services = {"application": {"image": "x", "container_name": "calutron-ronline"}}
assert hostnames(services) == ["application", "calutron-ronline"]

def test_empty_hostname_is_skipped(self) -> None:
services = {"a": {"image": "x", "hostname": ""}}
assert hostnames(services) == ["a"]
Expand Down
4 changes: 4 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,7 @@ def test_healthcheck_extension_key_is_accepted(self) -> None:
def test_service_hostname_is_accepted(self) -> None:
compose = {"services": {"keydb": {"image": "x", "hostname": "keydb-test-server-0"}}}
assert validate(compose) == []

def test_service_container_name_is_accepted(self) -> None:
compose = {"services": {"application": {"image": "x", "container_name": "calutron-ronline"}}}
assert validate(compose) == []