From 5bcdb8a5988f06d98067219bd0a605d67dcaeb18 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 17:48:28 +0300 Subject: [PATCH 1/2] docs: plan container_name support --- .../changes/2026-07-08.03-container-name.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 planning/changes/2026-07-08.03-container-name.md diff --git a/planning/changes/2026-07-08.03-container-name.md b/planning/changes/2026-07-08.03-container-name.md new file mode 100644 index 0000000..c93a276 --- /dev/null +++ b/planning/changes/2026-07-08.03-container-name.md @@ -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 :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 ` — 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 :127.0.0.1`. + +## Risk + +Low — identical mechanism to the already-shipped, tested `hostname` support. From da38aa6dd1128cc4018063a75eea9ce2f3f5fd53 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 8 Jul 2026 17:51:29 +0300 Subject: [PATCH 2/2] feat: accept and resolve the service container_name key Mirrors the existing hostname support: container_name is accepted in validate() and folded into graph.hostnames(), so it becomes an --add-host :127.0.0.1 entry. The actual podman container is still named {pod}-{service} internally; only name resolution is affected. --- architecture/supported-subset.md | 22 +++++++++++++--------- compose2pod/graph.py | 5 ++++- compose2pod/parsing.py | 1 + tests/test_emit.py | 19 +++++++++++++++++++ tests/test_graph.py | 4 ++++ tests/test_parsing.py | 4 ++++ 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/architecture/supported-subset.md b/architecture/supported-subset.md index 77165f7..5e8b338 100644 --- a/architecture/supported-subset.md +++ b/architecture/supported-subset.md @@ -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 diff --git a/compose2pod/graph.py b/compose2pod/graph.py index 25bed02..b1945b5 100644 --- a/compose2pod/graph.py +++ b/compose2pod/graph.py @@ -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(): diff --git a/compose2pod/parsing.py b/compose2pod/parsing.py index cf2a9c5..090bcc8 100644 --- a/compose2pod/parsing.py +++ b/compose2pod/parsing.py @@ -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"} diff --git a/tests/test_emit.py b/tests/test_emit.py index 497460e..db3b203 100644 --- a/tests/test_emit.py +++ b/tests/test_emit.py @@ -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", diff --git a/tests/test_graph.py b/tests/test_graph.py index 1e73d7e..339d843 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -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"] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 5ff0f29..cde9c4b 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -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) == []