Files
mesh-controller/internal/inventory/ports_test.go
T
jschoubben 1f5b70a995 The mesh assigns the port, and a module says it once
novox/hq ADR 0038. A module cannot choose a port: it is written once and
assigned anywhere, so any number it picks is a guess about a machine it
has never seen. A database module met the mesh's own store on 5432 and
was told, by a container runtime three layers down, that the port was
already allocated.

The number used to appear three times in every module — the rule set,
what a consumer is told, and what the runtime publishes — agreeing only
because one person wrote all three. Now it appears once, in `listens`,
and the other two are derived: the container publishes `20000:5432`, the
consumer is told 20000, and the rule set opens 20000.

An assignment is made once and kept, as a credential is. A port that
moved on every declaration would restart both ends each time and hand a
consumer a number that was true when it was read.

Ports the protocol fixes — mail on 25, submission on 587, DNS on 53 —
say so, and are then claims: one holder per machine, and the second is
refused by name at assignment. That is the mechanism the mesh already
has for what is singular on a machine, pointed at ports.

A mapping written the long way is left exactly as it is. Some things
must be pinned by hand, and quietly overruling somebody who wrote both
halves would be worse than not offering the short form.

Still open, and known: the substrate is not a module, so the mesh has
never heard of its own store and cannot yet assign around it. That is
what 028 will still be about after this.
2026-09-01 17:52:53 +02:00

152 lines
4.5 KiB
Go

package inventory
import (
"errors"
"testing"
"github.com/novox/mesh-control/internal/catalogue"
)
func aNodeWithModules(t *testing.T, modules ...string) (*Inventory, string) {
t.Helper()
inv := fresh(t)
ctx := t.Context()
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
t.Fatal(err)
}
for _, m := range modules {
if err := inv.RegisterModule(ctx,
catalogue.Manifest{Module: m, Version: "1"}, Source{}); err != nil {
t.Fatal(err)
}
}
return inv, "anchor"
}
// **Made once and kept.** A port that moved on every declaration would restart both ends each
// time, and would hand a consumer a number that was true when it was read.
func TestAPortIsAssignedOnceAndKept(t *testing.T) {
inv, node := aNodeWithModules(t, "postgres")
first, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
if err != nil {
t.Fatal(err)
}
second, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
if err != nil {
t.Fatal(err)
}
if first.Machine != second.Machine {
t.Fatalf("asking twice moved the port: %d then %d", first.Machine, second.Machine)
}
if first.Machine == 5432 {
t.Error("the mesh handed back the port the module asked for, which is what it cannot know is free")
}
}
// The fault this exists for: two modules wanting one number, which neither of them chose badly.
func TestTwoModulesWantingOnePortGetTwo(t *testing.T) {
inv, node := aNodeWithModules(t, "postgres", "another-database")
a, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
if err != nil {
t.Fatal(err)
}
b, err := inv.PortFor(t.Context(), node, "another-database", 5432, false)
if err != nil {
t.Fatal(err)
}
if a.Machine == b.Machine {
t.Fatalf("both were put on %d, which is the collision this exists to prevent", a.Machine)
}
}
// A port the protocol fixes is used as written, because a mail system elsewhere is not a mail
// system.
func TestAFixedPortIsTheOneTheProtocolSays(t *testing.T) {
inv, node := aNodeWithModules(t, "mailu")
got, err := inv.PortFor(t.Context(), node, "mailu", 25, true)
if err != nil {
t.Fatal(err)
}
if got.Machine != 25 {
t.Fatalf("mail was put on %d", got.Machine)
}
if !got.Fixed {
t.Error("it does not record that the protocol chose it, so nothing can refuse a second holder")
}
}
// **A fixed port is a claim**: one holder per machine, refused by name at assignment rather than
// by a container runtime at apply.
func TestASecondModuleCannotHaveAFixedPort(t *testing.T) {
inv, node := aNodeWithModules(t, "mailu", "other-mail")
if _, err := inv.PortFor(t.Context(), node, "mailu", 25, true); err != nil {
t.Fatal(err)
}
_, err := inv.PortFor(t.Context(), node, "other-mail", 25, true)
if err == nil {
t.Fatal("two modules were given port 25 on one machine")
}
if !errors.Is(err, ErrPortTaken) {
t.Errorf("the refusal is not the one a caller can recognise: %v", err)
}
if !contains(err.Error(), "mailu") {
t.Errorf("the refusal does not say who has it: %v", err)
}
}
// An assigned port must not land on one the protocol fixed for something else.
func TestAnAssignedPortAvoidsAFixedOne(t *testing.T) {
inv, node := aNodeWithModules(t, "mailu", "web")
fixed, err := inv.PortFor(t.Context(), node, "mailu", 20000, true)
if err != nil {
t.Fatal(err)
}
assigned, err := inv.PortFor(t.Context(), node, "web", 8080, false)
if err != nil {
t.Fatal(err)
}
if assigned.Machine == fixed.Machine {
t.Fatalf("an assignment landed on %d, which the protocol had fixed for something else",
fixed.Machine)
}
}
// What a module gave back is available again. Otherwise a machine that ran a hundred modules over
// a year has a hundred ports it cannot explain.
func TestUnassigningGivesThePortBack(t *testing.T) {
inv, node := aNodeWithModules(t, "postgres")
first, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
if err != nil {
t.Fatal(err)
}
if err := inv.ReleasePorts(t.Context(), node, "postgres"); err != nil {
t.Fatal(err)
}
held, err := inv.PortsFor(t.Context(), node)
if err != nil {
t.Fatal(err)
}
if len(held) != 0 {
t.Fatalf("it still holds %v", held)
}
again, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
if err != nil {
t.Fatal(err)
}
if again.Machine != first.Machine {
t.Errorf("the freed port was not the first one offered again: %d then %d",
first.Machine, again.Machine)
}
}
func contains(s, what string) bool {
return len(s) >= len(what) && (func() bool {
for i := 0; i+len(what) <= len(s); i++ {
if s[i:i+len(what)] == what {
return true
}
}
return false
})()
}