Carry the machine-assigned port in a plan's contribution, so a provider serving from plans dials where the module listens (issue 774, research 072 row 34)

route-proxy served every route at the software's own port (gitea's 3000, published at 20000;
grafana's 3000 on another machine, published at 20012) and public sites answered 502. The plan's
contribution now carries the port the placement assigned, the number ${port:N} gives the module,
and a composition for plans refuses a port none assigned by module, node and port.
This commit is contained in:
2026-10-31 13:08:35 +01:00
parent fbbfae7629
commit b611022457
5 changed files with 107 additions and 5 deletions
+46 -2
View File
@@ -221,6 +221,14 @@ type Rendering struct {
// They win over anything the mesh would assign and over a manifest's own long-form mapping.
Given map[string]map[int]int
// Unassigned is each port the mesh may assign that the placement holds no machine port for yet, by module and by the
// port the software uses: in Ports it stands as the software's own number, which nothing on the machine published.
Unassigned map[string]map[int]bool
// Planned says this rendering composes the plans the seat mesh-provisioner keeps (compose.Planning). A contribution a
// plan carries is then refused where its port has no machine port assigned (Ports, Given, Unassigned), never carried
// as the software's own number: its provider dials what the plan says (issue 774, research 072 row 34).
Planned bool
// Taken is the modules taken on this adopted node (novox/hq ADR 0100). The guard is derived
// from these only (ADR 0103): a port of a module assigned but not taken may still be the
// predecessor's.
@@ -1080,7 +1088,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
return nil, err
}
handed.name(m.Module, to, own)
contributed, err := r.contributedOf(m, to, with.Settings[m.Module])
contributed, err := r.contributedOf(m, to, with)
if err != nil {
return nil, err
}
@@ -1836,9 +1844,19 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
// file (contributions: the one of `contributes` with what the module shares of its bucket, then each of several by
// local name), with where m's machine is, encoded for PlanProvision.Contributed (issue 774): one composition, so a
// plan and a file cannot say different things. Nil where m contributes nothing to it.
func (r Resolution) contributedOf(m Manifest, to string, layers []Layer) (json.RawMessage, error) {
//
// **Its port is the one the machine published** (atAssignedPort): a plan is read on whatever machine its provider runs,
// which cannot redirect the number as a received file on this machine is redirected (atMachinePort), so the plan
// carries the machine port already. route-proxy dialled gitea's 3000 for git (published at 20000) and grafana's 3000
// on another machine (published at 20012) until it did (2026-10-31).
func (r Resolution) contributedOf(m Manifest, to string, with Rendering) (json.RawMessage, error) {
layers := with.Settings[m.Module]
form := ContributedForm{At: r.At}
add := func(local string, values map[string]any) error {
values, err := r.atAssignedPort(m, to, values, with)
if err != nil {
return err
}
raw, err := json.Marshal(values)
if err != nil {
return fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
@@ -1873,6 +1891,32 @@ func (r Resolution) contributedOf(m Manifest, to string, layers []Layer) (json.R
return EncodeContributed(form)
}
// atAssignedPort is a contribution of m with its port the machine port m's placement assigned it on this node, the
// number `${port:<software port>}` gives m itself (port_into.go): what its provider dials, on this machine over loopback
// and from another at m's address.
//
// **Refused, never guessed, where a plan carries it** (with.Planned): a port the placement holds no machine port for is
// said by module, node and port, since the software's own number is where nothing listens. Elsewhere (a declaration,
// a composition with no placement) a port not known is left as it is, as machinePort leaves it.
func (r Resolution) atAssignedPort(m Manifest, to string, values map[string]any, with Rendering) (map[string]any, error) {
number, ok := asPort(values["port"])
if !ok {
return values, nil
}
at, known := with.Given[m.Module][number]
if !known && !with.Unassigned[m.Module][number] {
at, known = with.Ports[m.Module][number]
}
if !known {
if with.Planned {
return nil, fmt.Errorf("%s contributing to %s: its port %d on %s has no machine port assigned in its placement, "+
"so its provider would dial the software's own port, where nothing listens", m.Module, to, number, r.Node)
}
return values, nil
}
return atMachinePort(values, m.Module, map[string]map[int]int{m.Module: {number: at}}), nil
}
// composed is one contribution as its provider receives it: settled with this node's settings, its
// endpoint's port filled in, and its names composed from the label.
//
+43
View File
@@ -114,6 +114,49 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
string(given.Each[1].Values) != `{"internal-name":"notify-api.anchor.internal","label":"notify-api","name":"notify-api.example.org","port":9000}` {
t.Fatalf("the plan gives %+v (%v)", given, err)
}
// Its port is the machine port the placement assigned, the number ${port:9000} gives the module, never the software's
// own (2026-10-31: route-proxy dialled gitea's 3000, published at 20000): its provider dials what the plan says, on
// this machine over loopback and from another at the plan's address. Where a plan carries it, a port with none
// assigned is refused by module, node and port.
for _, p := range []struct {
name string
ports map[string]map[int]int
given map[string]map[int]int
unassigned map[string]map[int]bool
planned bool
port float64
refused string
}{
{"assigned by the placement", map[string]map[int]int{"notify": {9000: 20009}}, nil, nil, true, 20009, ""},
{"given by the node over the placement", map[string]map[int]int{"notify": {9000: 20009}},
map[string]map[int]int{"notify": {9000: 9900}}, nil, true, 9900, ""},
{"the placement holds none (foreseen)", map[string]map[int]int{"notify": {9000: 9000}}, nil,
map[string]map[int]bool{"notify": {9000: true}}, true, 0, "notify contributing to route: its port 9000 on workstation has no machine port"},
{"the module's ports are not on the machine at all", nil, nil, nil, true, 0, "its port 9000 on workstation"},
{"a declaration with no placement leaves it as written", nil, nil, nil, false, 9000, ""},
} {
with := handingTo("store:5000")
with.Ports, with.Given, with.Unassigned, with.Planned = p.ports, p.given, p.unassigned, p.planned
c, err := r.Compose(with)
if p.refused != "" {
if err == nil || !strings.Contains(err.Error(), p.refused) {
t.Errorf("%s: composed with %v, want refused with %q", p.name, err, p.refused)
}
continue
}
if err != nil {
t.Errorf("%s: %v", p.name, err)
continue
}
given, err := DecodeContributed(c.Plans["notify"].Provisions[0].Contributed)
var api map[string]any
if err == nil {
err = json.Unmarshal(given.Each[1].Values, &api)
}
if err != nil || api["port"] != p.port || given.At != "workstation.internal" {
t.Errorf("%s: the plan gives %v at %q (%v), want port %v", p.name, api, given.At, err, p.port)
}
}
// Read strictly: a form the SDK does not write is refused, whatever its contributions say.
for raw, refused := range map[string]string{
`{"v":2,"each":[{"values":{}}]}`: "version 2",
+2
View File
@@ -58,6 +58,8 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
}
r := placed.Rendering
out.ForeseenPorts = placed.ForeseenPorts
// A plan carries its contributions' machine ports, refused where the placement assigned none (issue 774).
r.Planned = mode == Planning
r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound
out.Withheld, out.Unbound = withheld, unbound
for _, g := range grantsForeseen {
+7
View File
@@ -245,6 +245,13 @@ func (c *composer) portsFor(node Node, plan catalogue.Resolution, settings catal
where = at
} else {
*foreseen = append(*foreseen, fmt.Sprintf("%s:%d", m.Module, l.Port))
if r.Unassigned == nil {
r.Unassigned = map[string]map[int]bool{}
}
if r.Unassigned[m.Module] == nil {
r.Unassigned[m.Module] = map[int]bool{}
}
r.Unassigned[m.Module][l.Port] = true
}
}
if ports[m.Module] == nil {
+9 -3
View File
@@ -52,12 +52,12 @@ func TestAnAllocatedPortIsComposedAndAnUnallocatedOneIsNamedForeseenNotInvented(
if p := got.Rendering.Ports["web"][8080]; p != 8080 {
t.Errorf("a port nobody allocated is composed as %d, not as the manifest's own", p)
}
if len(got.ForeseenPorts) != 1 || got.ForeseenPorts[0] != "web:8080" {
t.Errorf("foreseen ports are %v", got.ForeseenPorts)
if len(got.ForeseenPorts) != 1 || got.ForeseenPorts[0] != "web:8080" || !got.Rendering.Unassigned["web"][8080] {
t.Errorf("foreseen ports are %v (unassigned %v)", got.ForeseenPorts, got.Rendering.Unassigned)
}
nodeOf(t, &in, "laptop").Ports = map[string]map[int]int{"web": {8080: 20123}}
got = placed(t, in, "laptop")
if p := got.Rendering.Ports["web"][8080]; p != 20123 || len(got.ForeseenPorts) != 0 {
if p := got.Rendering.Ports["web"][8080]; p != 20123 || len(got.ForeseenPorts) != 0 || got.Rendering.Unassigned != nil {
t.Errorf("the allocated port is composed as %d (foreseen %v)", p, got.ForeseenPorts)
}
// A port the module does not publish is not the mesh's to move, so it is never foreseen.
@@ -70,6 +70,12 @@ func TestAnAllocatedPortIsComposedAndAnUnallocatedOneIsNamedForeseenNotInvented(
t.Errorf("an unpublished port: %v foreseen %v", got.Rendering.Ports, got.ForeseenPorts)
}
unchanged(t, mk, func(in Input) { placed(t, in, "laptop") })
// Only a composition for plans refuses a contribution whose port none assigned (issue 774): a declaration still composes.
for _, mode := range []Mode{Reading, Foreseeing, Planning} {
if got := rendered(t, mk(), "laptop", mode).Rendering; got.Planned != (mode == Planning) {
t.Errorf("mode %d composes with Planned %v", mode, got.Planned)
}
}
}
// 1363/AC3: a port the operator gave is used, over the allocated one, and a consumer elsewhere is told it