The first pass answered under both ends, which review showed is the same fault seen from the other side where two mappings share a number. Verified on the machine: the forge is back on the port its own configuration has always advertised.
60 lines
3.0 KiB
Markdown
60 lines
3.0 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-23
|
|
located-in: [mesh-controller internal/catalogue/filtering.go, mesh-controller cmd/mesh-controller/plan.go]
|
|
fixed-by: mesh-controller — a node may move a port a module publishes as a mapping's machine side
|
|
amended-design:
|
|
---
|
|
|
|
# 094 — A port published as the machine side of a mapping cannot be moved, and trying blocks every push
|
|
|
|
## What was observed
|
|
|
|
On the control-node, immediately after the first module was migrated, 2026-09-23.
|
|
|
|
A module publishes two ports. One is written short, so the machine's side and the software's side
|
|
are the same number. The other is written long: **a machine port mapped to a different port inside
|
|
the container**, because the software's own port is one the machine's own daemon already holds.
|
|
The module's `listens` names the **machine** side of that mapping, deliberately, and says why.
|
|
|
|
The predecessor served that port on a different machine port. Moving the module's to match — the
|
|
whole point of a port being a per-node setting
|
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)) — was
|
|
refused:
|
|
|
|
```
|
|
gives port 2222 a machine port, and no container of its publishes 2222 —
|
|
the mesh cannot move a port the module does not publish
|
|
```
|
|
|
|
It does publish it. The check collects only the **last** segment of each published entry, which is
|
|
the container's inner port, so for a long-form mapping it sees the wrong half. Naming the inner
|
|
port instead is accepted and then never consulted, because the lookup is by what the module says
|
|
it listens on — which is the machine side.
|
|
|
|
**And the refusal is not confined to the setting.** While that setting exists, composition fails,
|
|
so **every push to that node is refused** until it is removed. A setting that cannot work should
|
|
be refused where it is set, not where it is read.
|
|
|
|
The same blind spot appears one step further on: on an adopted node **no opening was derived** for
|
|
that port either, and a per-node `expose` for it produced none, so the firewall never opened what
|
|
the module serves.
|
|
|
|
## Why it matters beyond this instance
|
|
|
|
Every module that maps a machine port to a different port inside its container has this hole, and
|
|
the pattern is common precisely where it matters: a service whose own port collides with something
|
|
the machine already runs. It is also exactly the case a migration hits, because matching the
|
|
predecessor's port is how a service keeps working when it changes hands.
|
|
|
|
The push-blocking half is worse than the feature gap. One unusable setting stops a machine being
|
|
told anything at all, and the message names a port rather than the setting that caused it.
|
|
|
|
## Open questions
|
|
|
|
- Should a setting name the port the software listens on, the machine port, or either, and what
|
|
refuses the ambiguous case where a module publishes both halves of some other mapping?
|
|
- Should `settings set` validate against the module's manifest at the moment it is set, so an
|
|
impossible setting cannot be stored?
|
|
- Should composition fail the whole node, or fail that module and send the rest?
|