Files
mesh-controller/internal/inventory/ports_test.go
T
jschoubben 41f7c51032 Assign around what a machine already holds
The other half of ADR 0038, and what 04-ISSUES/028 was actually about.
A module can now avoid colliding with another module; until this it
could not avoid colliding with the mesh itself.

The substrate is not a module. A node raises it from the bundle it
carries before any mesh exists, so the control plane had never heard of
the store, the broker, or its own container — and handed a database
module 5432, which the store already had.

So the machine says. The host records what each resource binds,
distinguishing what it carried from what the mesh sent — a distinction
that already existed so the two never remove each other — and reports
the carried ones. The node states and this context writes, which is the
shape of every message between them.

What the declaration binds, not what is open. A machine's open ports are
a moving target, and assigning around them would mean a port that was
free when it was asked for and taken when it was used.

Replaced whole each time rather than merged: a machine that gave a port
back must be believed about that too, and a set that only grows keeps a
port reserved for something no longer there.

Tested against a real database, and the tests bite — removing the check
hands the module 20000, which the machine had said it holds.
2026-09-01 18:32:38 +02:00

214 lines
6.7 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
})()
}
// **The bug this whole thing is for** (novox/hq 04-ISSUES/028). The mesh keeps its own store on a
// machine, from the bundle, before there is any mesh to ask. A database module assigned there was
// handed 5432 — the port the store already had — and found out from a container runtime.
func TestAModuleIsNotGivenAPortTheMachineAlreadyHolds(t *testing.T) {
inv, node := aNodeWithModules(t, "some-service")
ctx := t.Context()
// What the machine says it raised for itself: the store, the broker, the control plane.
if err := inv.RecordCarried(ctx, node, []int{5432, 5671, 8080, 20000, 20001}); err != nil {
t.Fatal(err)
}
got, err := inv.PortFor(ctx, node, "some-service", 5432, false)
if err != nil {
t.Fatal(err)
}
for _, held := range []int{5432, 5671, 8080, 20000, 20001} {
if got.Machine == held {
t.Fatalf("it was given %d, which the machine already holds", held)
}
}
if got.Machine != 20002 {
t.Errorf("expected the lowest free one, 20002, and got %d", got.Machine)
}
}
// A port the protocol fixes, already held by something the mesh did not put there, is refused —
// and refused here rather than by a container runtime on the machine.
func TestAFixedPortTheMachineAlreadyHoldsIsRefused(t *testing.T) {
inv, node := aNodeWithModules(t, "mailu")
ctx := t.Context()
if err := inv.RecordCarried(ctx, node, []int{25}); err != nil {
t.Fatal(err)
}
_, err := inv.PortFor(ctx, node, "mailu", 25, true)
if err == nil {
t.Fatal("a module was given a port something on the machine already holds")
}
if !contains(err.Error(), "already runs") {
t.Errorf("the refusal does not say the machine itself has it: %v", err)
}
}
// A machine that gave a port back is believed about that too.
func TestWhatAMachineNoLongerHoldsIsAvailableAgain(t *testing.T) {
inv, node := aNodeWithModules(t, "a-service")
ctx := t.Context()
if err := inv.RecordCarried(ctx, node, []int{20000}); err != nil {
t.Fatal(err)
}
if err := inv.RecordCarried(ctx, node, nil); err != nil {
t.Fatal(err)
}
got, err := inv.PortFor(ctx, node, "a-service", 1234, false)
if err != nil {
t.Fatal(err)
}
if got.Machine != 20000 {
t.Errorf("a port the machine gave back was still reserved: got %d", got.Machine)
}
}