Files
jschoubben b70f0d626a What review found in the port machinery, fixed
Three faults, one file split. All from reading, all verified to bite.

Unassign now releases the module's ports. ReleasePorts existed, said
"for when it is unassigned" in its own comment, and was called by
nothing — so a fixed port stayed claimed in the name of a module that
was gone, and the next module needing it was refused by a ghost.
Kept-once-chosen is a promise about a module that is still here.

MachineSide reads addressed mappings. "127.0.0.1:8080:80" was split at
the first colon, "127.0.0.1" failed to parse as a port, and the mapping
was silently skipped — putting the filter back on the declared port,
the exact fault the function was written to end. The machine side is
the second-from-last part, which is the reading the host already
applies, and the substrate bundle writes that shape today.

An allocation race answers in the mesh's words. Two concurrent picks of
the same port used to surface as a Postgres constraint violation,
verbatim. The table has two keys, so the collision is one of two facts:
the racer was this same assignment — then its answer is the answer,
kept-once-chosen does not care who chose — or another module took the
machine port, and an unfixed pick is simply made again against the
moved free list. A fixed port that lost the race is refused by name.
Told apart by re-reading the row, not by the constraint's name, so this
does not couple to the migration's spelling.

And the artifact-store cycle tests moved to bootstrap_cycle_test.go;
machineside_test.go had quietly become three subjects.
2026-09-01 21:54:05 +02:00

61 lines
2.5 KiB
Go

package catalogue
import "testing"
// A module with nothing that publishes binds what it binds, and the mesh may not move it.
//
// This is the case that made novox/hq 04-ISSUES/028's fix wrong on its first pass: a port was
// assigned to every module that declared one, so a service listening directly had the rule set
// opened on a number nothing was listening on, and its real port shut. The firewall reported
// success and blocked the service, which is the exact failure the mechanism exists to prevent.
func TestAPortNothingPublishesIsNotTheMeshsToMove(t *testing.T) {
m := Manifest{Module: "talker", Listens: []Listening{{Port: 9101, From: FromMesh}}}
at, mayAssign := m.MachineSide(9101)
if mayAssign {
t.Fatal("the mesh took a port it cannot move: nothing translates it, so assigning one " +
"opens the wrong number and leaves the service unreachable")
}
if at != 9101 {
t.Fatalf("a port nothing publishes reaches the machine where it binds, not at %d", at)
}
}
// A container publishing in short form is exactly the case the mesh may choose.
func TestAContainerPublishingShortIsTheMeshsToChoose(t *testing.T) {
m := Manifest{Module: "store", Resources: []map[string]any{
{"type": "container", "id": "server", "ports": []any{"5432"}},
}}
if _, mayAssign := m.MachineSide(5432); !mayAssign {
t.Fatal("a container's mapping is what translates a port, so this one is the mesh's to " +
"choose; refusing it puts every module back on a number it guessed")
}
}
// A manifest that wrote its own mapping already chose, and the machine side is the outer one.
func TestAMappingTheManifestWroteIsNotReassigned(t *testing.T) {
m := Manifest{Module: "mail", Resources: []map[string]any{
{"type": "container", "id": "front", "ports": []any{"7080:80"}},
}}
for _, named := range []int{7080, 80} {
at, mayAssign := m.MachineSide(named)
if mayAssign {
t.Fatalf("%d was reassigned though the manifest published it explicitly, which "+
"would open a rule on a port the container does not publish", named)
}
if at != 7080 {
t.Fatalf("naming %d gave %d; the machine side of 7080:80 is 7080", named, at)
}
}
}
// A port some other container publishes is not this port.
func TestAPortNotInTheMappingIsNotFound(t *testing.T) {
m := Manifest{Module: "mail", Resources: []map[string]any{
{"type": "container", "id": "front", "ports": []any{"25", "7080:80"}},
}}
if at, mayAssign := m.MachineSide(993); mayAssign || at != 993 {
t.Fatalf("993 is published by nothing here, so it binds where it binds: got %d, %v",
at, mayAssign)
}
}