|
| 1 | +--- |
| 2 | +summary: The emit-side --add-host set is scoped to the target's dependency closure, so a service that never runs no longer resolves to 127.0.0.1 and can no longer veto another service's extra_hosts. |
| 3 | +--- |
| 4 | + |
| 5 | +# Change: Scope --add-host to the target's closure |
| 6 | + |
| 7 | +**Lane:** lightweight — one-line change in `emit._plan`, plus tests. |
| 8 | + |
| 9 | +## Goal |
| 10 | + |
| 11 | +`--add-host` is the one aggregate in the emit path that is built document-wide |
| 12 | +instead of closure-scoped, and the seam shows. `emit._plan` seeds hosts from |
| 13 | +`graph.hostnames(services)` — every service in the *document* — while |
| 14 | +`extra_hosts` is layered per service in `order`, the target's dependency |
| 15 | +closure. `pod._add_host_flags` then conflict-checks the two against each other, |
| 16 | +so a service that **never runs** can veto a valid configuration: |
| 17 | + |
| 18 | +```yaml |
| 19 | +services: |
| 20 | + app: {image: i, extra_hosts: ["db:1.2.3.4"]} |
| 21 | + other: {image: i, hostname: db} # not in app's closure; never started |
| 22 | +``` |
| 23 | +→ `UnsupportedComposeError: service 'app': conflicting host 'db' |
| 24 | +('127.0.0.1' vs '1.2.3.4')` |
| 25 | + |
| 26 | +The second symptom is quieter: a never-run service still gets an `--add-host` |
| 27 | +entry pointing its name at `127.0.0.1`, where nothing is listening. That turns |
| 28 | +an honest name-resolution failure into a connection-refused. |
| 29 | + |
| 30 | +Every other aggregate in the emit path — `dns`, `dns_search`, `dns_opt`, |
| 31 | +`sysctls`, secrets, configs — is closure-scoped. This makes `--add-host` agree. |
| 32 | + |
| 33 | +## Approach |
| 34 | + |
| 35 | +`emit._plan` passes only the closure's services to `hostnames()`: |
| 36 | + |
| 37 | +```python |
| 38 | +hosts = hostnames({name: services[name] for name in order}) |
| 39 | +``` |
| 40 | + |
| 41 | +`hostnames()` has exactly two callers, and only this one changes: |
| 42 | + |
| 43 | +- `parsing.py` calls it document-wide to shape-check every service's |
| 44 | + `hostname`/`container_name`/`networks` at the gate. That stays — validation |
| 45 | + is target-agnostic, and scoping it would stop rejecting a malformed |
| 46 | + `hostname` on a service outside the closure. |
| 47 | +- `emit.py` calls it to build the `--add-host` set. That is the one that must |
| 48 | + match the pod's actual contents. |
| 49 | + |
| 50 | +Truth home: `architecture/supported-subset.md`'s Pod-level options section, |
| 51 | +which currently documents the document-wide behavior as "pre-existing, |
| 52 | +orthogonal". |
| 53 | +
|
| 54 | +## Files |
| 55 | +
|
| 56 | +- `compose2pod/emit.py` — scope the `hostnames()` argument to `order` |
| 57 | +- `architecture/supported-subset.md` — Pod-level options: `--add-host` is |
| 58 | + closure-scoped like the rest |
| 59 | +- `tests/test_emit.py` — tests added |
| 60 | +
|
| 61 | +## Verification |
| 62 | +
|
| 63 | +- [ ] Failing test first: an out-of-closure `hostname` colliding with an |
| 64 | + in-closure `extra_hosts` must NOT raise; a never-run service must NOT |
| 65 | + appear in `--add-host`. |
| 66 | +- [ ] Apply the change. |
| 67 | +- [ ] Tests pass. |
| 68 | +- [ ] `just test-ci` — full suite green at 100%. |
| 69 | +- [ ] `just lint-ci`, `just check-planning` — clean. |
0 commit comments