From b611022457063390f96190e5ea1f3de83784d8ba Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 13:08:35 +0100 Subject: [PATCH 1/2] 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. --- go/catalogue/declaration.go | 48 ++++++++++++++++++++++++++++++++++-- go/catalogue/keyed_test.go | 43 ++++++++++++++++++++++++++++++++ go/compose/render.go | 2 ++ go/compose/rendering.go | 7 ++++++ go/compose/rendering_test.go | 12 ++++++--- 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/go/catalogue/declaration.go b/go/catalogue/declaration.go index d109f23..83206d9 100644 --- a/go/catalogue/declaration.go +++ b/go/catalogue/declaration.go @@ -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:}` 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. // diff --git a/go/catalogue/keyed_test.go b/go/catalogue/keyed_test.go index a07167f..21716ae 100644 --- a/go/catalogue/keyed_test.go +++ b/go/catalogue/keyed_test.go @@ -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", diff --git a/go/compose/render.go b/go/compose/render.go index 4dd0ab1..5eb1ea2 100644 --- a/go/compose/render.go +++ b/go/compose/render.go @@ -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 { diff --git a/go/compose/rendering.go b/go/compose/rendering.go index d942bc6..f658a33 100644 --- a/go/compose/rendering.go +++ b/go/compose/rendering.go @@ -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 { diff --git a/go/compose/rendering_test.go b/go/compose/rendering_test.go index 0f6d09b..465e649 100644 --- a/go/compose/rendering_test.go +++ b/go/compose/rendering_test.go @@ -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 From bb01a656d0249d5a0c97b19669bb83fa0dc60881 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 13:21:37 +0100 Subject: [PATCH 2/2] Carry an unassigned port as the declaration publishes it, refusing only a port a module does not listen on (issue 774, research 072 row 34) influxdb and unifi publish bare ports that mesh-placement assigns none: the machine runs them at the software's own number, so a plan refusing them held their whole plan for a route that works. --- go/catalogue/declaration.go | 34 +++++++++++++++------------------ go/catalogue/keyed_test.go | 37 ++++++++++++++++++------------------ go/compose/rendering.go | 7 ------- go/compose/rendering_test.go | 8 ++++---- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/go/catalogue/declaration.go b/go/catalogue/declaration.go index 83206d9..c0d9564 100644 --- a/go/catalogue/declaration.go +++ b/go/catalogue/declaration.go @@ -221,12 +221,9 @@ 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). + // plan carries is then refused where its port is none the module listens on: no machine port stands for it, and 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 @@ -1891,30 +1888,29 @@ func (r Resolution) contributedOf(m Manifest, to string, with Rendering) (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:}` gives m itself (port_into.go): what its provider dials, on this machine over loopback -// and from another at m's address. +// atAssignedPort is a contribution of m with its port where this machine publishes it for m: the number +// `${port:}` gives m itself and its declaration publishes (machinePort, port_into.go), the placement's +// assignment where it holds one and the software's own number where it holds none (a bare port not yet moved, a fixed +// mapping, the host's network). 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. +// **Refused where a plan carries a port m does not listen on** (with.Planned): no machine port stands for it, so the +// provider would dial a number nothing on the machine is known to publish. Elsewhere (a declaration, a composition +// with no ports) such a port 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 { + _, given := with.Given[m.Module][number] + _, placed := with.Ports[m.Module][number] + if !given && !placed { 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 nil, fmt.Errorf("%s contributing to %s: its port %d on %s is none of the ports it listens on, so no "+ + "machine port stands for it", m.Module, to, number, r.Node) } return values, nil } - return atMachinePort(values, m.Module, map[string]map[int]int{m.Module: {number: at}}), nil + return atMachinePort(values, m.Module, map[string]map[int]int{m.Module: {number: with.machinePort(m.Module, number)}}), nil } // composed is one contribution as its provider receives it: settled with this node's settings, its diff --git a/go/catalogue/keyed_test.go b/go/catalogue/keyed_test.go index 21716ae..7ca0cb7 100644 --- a/go/catalogue/keyed_test.go +++ b/go/catalogue/keyed_test.go @@ -114,29 +114,30 @@ 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. + // Its port is where the machine publishes it, the number ${port:9000} gives the module and its declaration publishes, + // never the software's own where the placement moved it (2026-10-31: route-proxy dialled gitea's 3000, published at + // 20000): its provider dials what the plan says. Where a plan carries it, a port the module does not listen on 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 + name string + ports map[string]map[int]int + given map[string]map[int]int + planned bool + port float64 + refused string }{ - {"assigned by the placement", map[string]map[int]int{"notify": {9000: 20009}}, nil, nil, true, 20009, ""}, + {"assigned by the placement (gitea 3000 at 20000)", map[string]map[int]int{"notify": {9000: 20009}}, 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, ""}, + map[string]map[int]int{"notify": {9000: 9900}}, true, 9900, ""}, + {"a bare published port the placement holds none for keeps its number (influxdb 8086)", + map[string]map[int]int{"notify": {9000: 9000}}, nil, true, 9000, ""}, + {"a mapping the module fixed (as mailu 7080:80)", map[string]map[int]int{"notify": {9000: 7080}}, nil, true, 7080, ""}, + {"a port the module does not listen on", nil, nil, true, 0, + "notify contributing to route: its port 9000 on workstation is none of the ports it listens on"}, + {"a declaration with no ports leaves it as written", nil, nil, false, 9000, ""}, } { with := handingTo("store:5000") - with.Ports, with.Given, with.Unassigned, with.Planned = p.ports, p.given, p.unassigned, p.planned + with.Ports, with.Given, with.Planned = p.ports, p.given, p.planned c, err := r.Compose(with) if p.refused != "" { if err == nil || !strings.Contains(err.Error(), p.refused) { diff --git a/go/compose/rendering.go b/go/compose/rendering.go index f658a33..d942bc6 100644 --- a/go/compose/rendering.go +++ b/go/compose/rendering.go @@ -245,13 +245,6 @@ 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 { diff --git a/go/compose/rendering_test.go b/go/compose/rendering_test.go index 465e649..7ba4a88 100644 --- a/go/compose/rendering_test.go +++ b/go/compose/rendering_test.go @@ -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" || !got.Rendering.Unassigned["web"][8080] { - t.Errorf("foreseen ports are %v (unassigned %v)", got.ForeseenPorts, got.Rendering.Unassigned) + if len(got.ForeseenPorts) != 1 || got.ForeseenPorts[0] != "web:8080" { + t.Errorf("foreseen ports are %v", got.ForeseenPorts) } 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 || got.Rendering.Unassigned != nil { + if p := got.Rendering.Ports["web"][8080]; p != 20123 || len(got.ForeseenPorts) != 0 { 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,7 +70,7 @@ 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. + // Only a composition for plans refuses a contribution whose port the module does not listen on (issue 774). 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)