Review found the first pass aliased its answer under both ends of a mapping, which is wrong wherever two mappings share a number: the alias lands on a key belonging to another mapping, the later write wins, and the filter and the container then disagree — the very fault this change exists to close. Two reproduced cases: a module publishing 8080:80 beside 9090:8080 had an explicit setting silently overwritten; a module publishing 4001:80 beside 4002:80 composed both containers onto one machine port, where before it was safely refused. Now a mapping's answer is filed once, under the end the module names in its listens — the number the plan, the filter, the openings, the guard and the consumer all ask for — and a key that names two mappings is refused in the same words as a setting that does. Also: the guard assertion in the end-to-end test failed open when the resource was absent; the plan-mirroring helper now says it stands in only where the plan does not allocate, and the assertions it feeds are narrowed to the port under test.
306 lines
14 KiB
Go
306 lines
14 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The catalogue's foundation modules as they are, parsed by the real parser (novox/hq ADR 0100):
|
|
// the store and the broker say which of their ports the mesh guards on an adopted node, and the
|
|
// filter module loads its table through a unit of its own whose stop deletes only that table.
|
|
func catalogueManifest(t *testing.T, module string) Manifest {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("../../../mesh-catalog/modules/" + module + "/module.json")
|
|
if err != nil {
|
|
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
|
}
|
|
m, err := ParseManifest(raw)
|
|
if err != nil {
|
|
t.Fatalf("%s does not parse:\n%v", module, err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestTheStoreAndTheBrokerSayWhatTheMeshGuards(t *testing.T) {
|
|
if got := catalogueManifest(t, "postgres").Guards; !reflect.DeepEqual(got, []int{5432}) {
|
|
t.Errorf("postgres guards %v; the store's port must be refused from outside", got)
|
|
}
|
|
if got := catalogueManifest(t, "lavinmq").Guards; !reflect.DeepEqual(got, []int{15672}) {
|
|
t.Errorf("lavinmq guards %v; the management port must be refused from outside", got)
|
|
}
|
|
}
|
|
|
|
func TestTheFilterModuleNeverFlushesTheRuleset(t *testing.T) {
|
|
m := catalogueManifest(t, "nftables")
|
|
var unit, stock, load map[string]any
|
|
for _, r := range m.Resources {
|
|
switch r["id"] {
|
|
case "unit":
|
|
unit = r
|
|
case "stock-unit-stop":
|
|
stock = r
|
|
case "load":
|
|
load = r
|
|
}
|
|
}
|
|
if load == nil || load["unit"] != "mesh-filter.service" {
|
|
t.Fatalf("the filter is not loaded by its own unit: %v", load)
|
|
}
|
|
content, _ := unit["content"].(string)
|
|
if unit == nil || unit["path"] != "/etc/systemd/system/mesh-filter.service" {
|
|
t.Fatalf("the filter's unit is not written: %v", unit)
|
|
}
|
|
if strings.Contains(content, "flush") {
|
|
t.Fatalf("stopping the filter flushes the whole ruleset — the runtime's and the found "+
|
|
"firewall's with it:\n%s", content)
|
|
}
|
|
if !strings.Contains(content, "ExecStop=nft delete table inet mesh\n") ||
|
|
!strings.Contains(content, "ExecStart=nft -f "+m.Filtering.Into+"\n") {
|
|
t.Fatalf("the unit does not load the computed rule set and delete only its own table:\n%s",
|
|
content)
|
|
}
|
|
// A node converged before the filter had its own unit still has the stock nftables.service
|
|
// enabled, whose stop flushes the whole ruleset: a drop-in makes it delete only the mesh's
|
|
// table, and the load is restarted on it so the host reloads units and the drop-in is read.
|
|
if stock == nil || stock["path"] != "/etc/systemd/system/nftables.service.d/mesh.conf" ||
|
|
!strings.HasSuffix(fmt.Sprint(stock["content"]),
|
|
"[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n") {
|
|
t.Fatalf("the stock unit's stop is not replaced with deleting the mesh's table: %v", stock)
|
|
}
|
|
// A changed rule set is RELOADED — ExecReload replaces the table in one `nft -f`, so the node
|
|
// is never unfiltered — and only the units themselves restart it, which is the one change a
|
|
// reload cannot carry.
|
|
if !reflect.DeepEqual(load["reload-on"], []any{"filtering"}) {
|
|
t.Fatalf("the filter is restarted rather than reloaded when its rules change, leaving the "+
|
|
"node unfiltered in between: %v", load)
|
|
}
|
|
if !reflect.DeepEqual(load["restart-on"], []any{"unit", "stock-unit-stop"}) {
|
|
t.Fatalf("the filter is not restarted when its unit or the stock unit's drop-in changes: %v",
|
|
load["restart-on"])
|
|
}
|
|
}
|
|
|
|
// The package registry's port is the node's, like every other foundation port (novox/hq
|
|
// 04-ISSUES/085, ADR 0100). Two halves, because the forge is reached two ways: through what the
|
|
// module that serves it says it serves, and — for the genesis window, before any module provides
|
|
// `package-registry` at all — through the one binding the builder carries instead of resolving.
|
|
|
|
func TestTheForgesPortIsGivenLikeAnyOtherProvidersPort(t *testing.T) {
|
|
forge := catalogueManifest(t, "gitea")
|
|
|
|
// The catalogue's number is a default and the node's setting moves it.
|
|
given, err := GivenPorts(forge, []Layer{{From: "anchor",
|
|
Values: map[string]any{PortsSetting: map[string]any{"3000": float64(3100)}}}})
|
|
if err != nil {
|
|
t.Fatalf("the forge's port cannot be given on a node: %v", err)
|
|
}
|
|
if given[3000] != 3100 {
|
|
t.Fatalf("the forge was given %v", given)
|
|
}
|
|
|
|
// And every consumer of the package registry is told where the machine actually put it,
|
|
// because that is read from what the forge serves rather than written in the consumer.
|
|
if got := ServedOn(forge, "package-registry", given)["port"]; got != 3100 {
|
|
t.Errorf("the package registry is served on %v, not the port this node gave it", got)
|
|
}
|
|
if got := ServedOn(forge, "package-registry", nil)["port"]; got != float64(3000) {
|
|
t.Errorf("without a setting the forge serves %v, not the catalogue's port", got)
|
|
}
|
|
}
|
|
|
|
// bindingIn is the package binding the builder carries, as the machine would receive it.
|
|
func bindingIn(t *testing.T, m Manifest, layers []Layer) map[string]any {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["id"]) != "package-binding" {
|
|
continue
|
|
}
|
|
settled, err := ApplySettings(r, layers)
|
|
if err != nil {
|
|
t.Fatalf("the builder's package binding refused %v: %v", layers, err)
|
|
}
|
|
if settled["merge"] != nil || settled["protected"] != nil {
|
|
t.Fatal("the host would be sent fields it does not know")
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal([]byte(fmt.Sprint(settled["content"])), &out); err != nil {
|
|
t.Fatalf("the builder's package binding is not a binding: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
t.Fatal("the builder carries no package binding")
|
|
return nil
|
|
}
|
|
|
|
func TestTheBuildersCarriedPackageBindingTakesThePortFromTheNode(t *testing.T) {
|
|
builder := catalogueManifest(t, "builder")
|
|
|
|
// Nothing set: the catalogue's own number, which is what a mesh raised on the defaults uses.
|
|
serves := bindingIn(t, builder, nil)["serves"].(map[string]any)
|
|
if serves["port"] != float64(3000) {
|
|
t.Fatalf("the builder's binding defaults to %v", serves["port"])
|
|
}
|
|
|
|
// Given a port, the binding dials it — and the rest of what the forge serves survives, because
|
|
// a setting is merged into the module's own values rather than replacing them.
|
|
moved := bindingIn(t, builder, []Layer{{From: "anchor",
|
|
Values: map[string]any{"serves": map[string]any{"port": float64(3100)}}}})
|
|
got := moved["serves"].(map[string]any)
|
|
if got["port"] != float64(3100) {
|
|
t.Errorf("the builder dials %v, not the port this node gave the package registry", got["port"])
|
|
}
|
|
if got["scheme"] != "http" || got["npm-path"] != "/api/packages/novox/npm/" {
|
|
t.Errorf("setting the port lost the rest of what the forge serves: %v", got)
|
|
}
|
|
if moved["as"] != "mesh-builder" || moved["from"] != "gitea" {
|
|
t.Errorf("setting the port changed who the binding is with: %v", moved)
|
|
}
|
|
}
|
|
|
|
// The two halves are one number. The builder carries a binding because at genesis nothing provides
|
|
// `package-registry` to resolve one from; the day the forge is a module, the same consumer is told
|
|
// what the forge serves. They have to start from the same port, or a mesh raised on the defaults
|
|
// dials one number before the forge is assigned and another after.
|
|
func TestTheBuildersCarriedBindingStartsWhereTheForgeServes(t *testing.T) {
|
|
forge := ServedOn(catalogueManifest(t, "gitea"), "package-registry", nil)
|
|
carried := bindingIn(t, catalogueManifest(t, "builder"), nil)["serves"].(map[string]any)
|
|
for _, key := range []string{"port", "scheme", "npm-path"} {
|
|
if fmt.Sprint(forge[key]) != fmt.Sprint(carried[key]) {
|
|
t.Errorf("the forge serves %s %v and the builder's carried binding says %v — the two "+
|
|
"halves of the same registry have drifted apart in the catalogue",
|
|
key, forge[key], carried[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheBuildersPackageBindingKeepsItsIdentity(t *testing.T) {
|
|
builder := catalogueManifest(t, "builder")
|
|
// `at` above all: a setting that moves it points the builder, and the registry password it
|
|
// sends as basic auth, at a host somebody else chose.
|
|
for _, key := range []string{"provision", "from", "at", "as"} {
|
|
var refused error
|
|
for _, r := range builder.Resources {
|
|
if fmt.Sprint(r["id"]) != "package-binding" {
|
|
continue
|
|
}
|
|
_, refused = ApplySettings(r, []Layer{{From: "anchor",
|
|
Values: map[string]any{key: "something else"}}})
|
|
}
|
|
if refused == nil {
|
|
t.Errorf("%q can be set on the builder's package binding, which is not a port but who "+
|
|
"the binding is with", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **And the forge's own address follows it**, composed from the manifest in the catalogue beside
|
|
// this checkout (novox/hq 04-ISSUES/088).
|
|
//
|
|
// The forge is reached a third way that neither test above covers: by its own sidecar, over the
|
|
// machine's loopback, told where to go in its environment. The `2999:3000` mapping that lets the
|
|
// forge go on binding 3000 does nothing for a caller dialling the machine — so a literal there is
|
|
// wrong on every node whose assignment differs, and wrong for a second reason on a node given the
|
|
// port (ADR 0100). Composed through the whole path, because what proves the placeholder resolves
|
|
// in an `env` at all is a declaration, not a manifest.
|
|
func TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt(t *testing.T) {
|
|
forge, err := catalogueManifest(t, "gitea").Resolve([]Built{{
|
|
Name: "runtime", Kind: ArtifactImage,
|
|
Reference: "registry.example/gitea-runtime@sha256:" + strings.Repeat("a", 64),
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("the forge's manifest does not resolve against its own build: %v", err)
|
|
}
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{forge}, Needs: []Needed{
|
|
{Name: "postgres-database", For: "gitea", From: "anchor", At: "127.0.0.1",
|
|
Serves: map[string]any{"port": float64(5432)}, Sealed: "sealed-db"},
|
|
{Name: "route", For: "gitea", From: "anchor"},
|
|
{Name: "secret", For: "gitea", From: "anchor", Local: "internal-token", Sealed: "sealed-token"},
|
|
{Name: "secret", For: "gitea", From: "anchor", Local: "admin", Sealed: "sealed-admin"},
|
|
}}
|
|
|
|
// The number this node was given for the forge — the one the machine it is about to run on
|
|
// already publishes.
|
|
out, err := r.Declaration(Rendering{
|
|
Needed: map[string]map[string]string{"gitea": {"broker": "sealed-broker"}},
|
|
Given: map[string]map[int]int{"gitea": {3000: 2999}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the forge does not compose: %v", err)
|
|
}
|
|
|
|
// What the machine publishes, and what the forge's sidecar is told to dial: one number.
|
|
server := fileNamed(out, "gitea.server")
|
|
if server == nil {
|
|
t.Fatalf("the forge's own container is not in the declaration: %v", out)
|
|
}
|
|
if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "2999:3000") {
|
|
t.Fatalf("the forge is not published on the port this node gave it: %v", server["ports"])
|
|
}
|
|
runtime := fileNamed(out, "gitea.runtime")
|
|
if runtime == nil {
|
|
t.Fatalf("the forge's sidecar is not in the declaration: %v", out)
|
|
}
|
|
env, _ := runtime["env"].(map[string]any)
|
|
if env["MESH_GITEA_URL"] != "http://127.0.0.1:2999" {
|
|
t.Fatalf("the forge's sidecar dials %v while the machine publishes the forge on 2999 — "+
|
|
"whatever reads it dials a dead port", env["MESH_GITEA_URL"])
|
|
}
|
|
}
|
|
|
|
// **And the port the forge publishes the long way is the node's too** (novox/hq ADR 0100).
|
|
//
|
|
// The forge's ssh port is written `2222:22` — the machine's own daemon holds 22, so the module
|
|
// takes 2222 and says so in `listens`. A node whose predecessor served git on another number
|
|
// cannot be told to leave it there unless the setting may name the machine side of that mapping,
|
|
// which is the number the manifest itself uses everywhere else. Composed from the manifest in the
|
|
// catalogue beside this checkout, because what the mesh can move is a fact about what the module
|
|
// actually writes.
|
|
func TestTheForgesSshPortIsGivenByTheNumberTheForgeCallsIt(t *testing.T) {
|
|
forge := catalogueManifest(t, "gitea")
|
|
given, err := GivenPorts(forge, []Layer{{From: "anchor",
|
|
Values: map[string]any{PortsSetting: map[string]any{"2222": float64(222)}}}})
|
|
if err != nil {
|
|
t.Fatalf("the forge's ssh port cannot be given on a node: %v", err)
|
|
}
|
|
// Under the number the module listens on — 2222, the machine side of its mapping — which is
|
|
// the number the plan, the filter, the openings and the consumer all ask for. One entry.
|
|
if want := map[int]int{2222: 222}; !reflect.DeepEqual(given, want) {
|
|
t.Fatalf("the forge was given %v, and it names its ssh port %v", given, want)
|
|
}
|
|
|
|
resolved, err := forge.Resolve([]Built{{
|
|
Name: "runtime", Kind: ArtifactImage,
|
|
Reference: "registry.example/gitea-runtime@sha256:" + strings.Repeat("a", 64),
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("the forge's manifest does not resolve against its own build: %v", err)
|
|
}
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{resolved}, Needs: []Needed{
|
|
{Name: "postgres-database", For: "gitea", From: "anchor", At: "127.0.0.1",
|
|
Serves: map[string]any{"port": float64(5432)}, Sealed: "sealed-db"},
|
|
{Name: "route", For: "gitea", From: "anchor"},
|
|
{Name: "secret", For: "gitea", From: "anchor", Local: "internal-token", Sealed: "sealed-token"},
|
|
{Name: "secret", For: "gitea", From: "anchor", Local: "admin", Sealed: "sealed-admin"},
|
|
}}
|
|
out, err := r.Declaration(Rendering{
|
|
Needed: map[string]map[string]string{"gitea": {"broker": "sealed-broker"}},
|
|
Ports: map[string]map[int]int{"gitea": {3000: 3000, 2222: 222}},
|
|
Given: map[string]map[int]int{"gitea": given},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the forge does not compose: %v", err)
|
|
}
|
|
server := fileNamed(out, "gitea.server")
|
|
if server == nil {
|
|
t.Fatalf("the forge's own container is not in the declaration: %v", out)
|
|
}
|
|
if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "222:22") ||
|
|
strings.Contains(published, "2222:22") {
|
|
t.Fatalf("the forge is published on %v, not the port this node gave it", server["ports"])
|
|
}
|
|
}
|