Merge pull request 'A contribution carries the machine port of its endpoint, so a provider serving from plans dials what the module binds (issue 774)' (#149) from fix/route-ports into main
This commit was merged in pull request #149.
This commit is contained in:
@@ -221,6 +221,11 @@ type Rendering struct {
|
|||||||
// They win over anything the mesh would assign and over a manifest's own long-form mapping.
|
// They win over anything the mesh would assign and over a manifest's own long-form mapping.
|
||||||
Given map[string]map[int]int
|
Given map[string]map[int]int
|
||||||
|
|
||||||
|
// 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 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
|
// 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
|
// from these only (ADR 0103): a port of a module assigned but not taken may still be the
|
||||||
// predecessor's.
|
// predecessor's.
|
||||||
@@ -1080,7 +1085,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handed.name(m.Module, to, own)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1836,9 +1841,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
|
// 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
|
// 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.
|
// 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}
|
form := ContributedForm{At: r.At}
|
||||||
add := func(local string, values map[string]any) error {
|
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)
|
raw, err := json.Marshal(values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
return fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
||||||
@@ -1873,6 +1888,31 @@ func (r Resolution) contributedOf(m Manifest, to string, layers []Layer) (json.R
|
|||||||
return EncodeContributed(form)
|
return EncodeContributed(form)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// atAssignedPort is a contribution of m with its port where this machine publishes it for m: the number
|
||||||
|
// `${port:<software 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 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
|
||||||
|
}
|
||||||
|
_, 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 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: with.machinePort(m.Module, number)}}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// composed is one contribution as its provider receives it: settled with this node's settings, its
|
// 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.
|
// endpoint's port filled in, and its names composed from the label.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -114,6 +114,50 @@ 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}` {
|
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)
|
t.Fatalf("the plan gives %+v (%v)", given, err)
|
||||||
}
|
}
|
||||||
|
// 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
|
||||||
|
planned bool
|
||||||
|
port float64
|
||||||
|
refused string
|
||||||
|
}{
|
||||||
|
{"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}}, 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.Planned = p.ports, p.given, 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.
|
// Read strictly: a form the SDK does not write is refused, whatever its contributions say.
|
||||||
for raw, refused := range map[string]string{
|
for raw, refused := range map[string]string{
|
||||||
`{"v":2,"each":[{"values":{}}]}`: "version 2",
|
`{"v":2,"each":[{"values":{}}]}`: "version 2",
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
|
|||||||
}
|
}
|
||||||
r := placed.Rendering
|
r := placed.Rendering
|
||||||
out.ForeseenPorts = placed.ForeseenPorts
|
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
|
r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound
|
||||||
out.Withheld, out.Unbound = withheld, unbound
|
out.Withheld, out.Unbound = withheld, unbound
|
||||||
for _, g := range grantsForeseen {
|
for _, g := range grantsForeseen {
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ func TestAnAllocatedPortIsComposedAndAnUnallocatedOneIsNamedForeseenNotInvented(
|
|||||||
t.Errorf("an unpublished port: %v foreseen %v", got.Rendering.Ports, got.ForeseenPorts)
|
t.Errorf("an unpublished port: %v foreseen %v", got.Rendering.Ports, got.ForeseenPorts)
|
||||||
}
|
}
|
||||||
unchanged(t, mk, func(in Input) { placed(t, in, "laptop") })
|
unchanged(t, mk, func(in Input) { placed(t, in, "laptop") })
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1363/AC3: a port the operator gave is used, over the allocated one, and a consumer elsewhere is told it
|
// 1363/AC3: a port the operator gave is used, over the allocated one, and a consumer elsewhere is told it
|
||||||
|
|||||||
Reference in New Issue
Block a user