Implements novox/hq ADR 0110 and 0111. The seat set lives in internal/catalogue/seats.go: fourteen seats, each with a scope, what occupying it delivers, and the record that made it one. A test asserts the count and a decision per entry, so changing the set means finding the argument, as the host's vocabulary test does. The first set is every seat already claimed — including the-private-network, which the network module claims from a manifest composed in this repository's code, not from any module.json — plus npm-package-registry (ADR 0109) and git (ADR 0111). A test parses every catalogue manifest and this repository's own and fails on any refused claim, so closing the set refuses nothing in use. ParseManifest now refuses a claim on a seat the mesh does not define, a seat claimed at another scope, and a delivering seat claimed by a module that does not provide what it delivers. A malformed claim is refused once, for being malformed. Resolution: among several providers of a mesh provision, a pin still wins; then the holder of the seat that delivers it; then the only provider; otherwise refused as before. ADR 0009's "never guessed" holds — the seat is the choice made once, mesh-wide, rather than a pin per consumer node. A provider now carries the module it came from, because a provider is a (node, module) pair and the pair is what tells a holder from a neighbour on the same machine. The planner's second pass is now given the first pass's holdings. Without them, a node consuming a seat-delivered provision was refused there, and a refused node's own claims dropped out of what the mesh holds — letting a second holder of one of its seats pass unrefused. `seats [--json]` lists every seat, what it delivers, and each holder, derived from assignments every time and never stored. Unheld seats are listed. A stored claim outside the set — possible for a manifest registered before the set closed, since stored manifests are not re-validated — is shown rather than hidden. `build --self <owner>/<repo>` builds from a repository on the git seat's holder. The clone URL is composed at build time from the holder's node and what it serves for git; the recorded source is the path and the seat (migration 0032), never an address, so a moved forge changes nothing recorded. Nobody holding the seat refuses self-hosted builds and says so; external URLs are unchanged. An address passed with --self is refused rather than recorded as a path. Replaces three foundation tests that defended the builder's carried package binding. The catalogue removed that binding when the builder began requiring the registry through a real grant, so the tests were already failing on main; they now assert the builder requires what the npm seat delivers and carries no copy of its own, and that the forge holds the npm and git seats. Verified: go vet clean; the whole suite passes against a throwaway Postgres (make postgres), the new inventory tests included; gofmt clean apart from cmd/mesh-builder/stdout_test.go, which fails on main too.
304 lines
14 KiB
Go
304 lines
14 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"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). The forge is reached through what it says it serves, and consumers —
|
|
// the builder among them — are told that, rather than carrying a number of their own.
|
|
|
|
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, "npm-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, "npm-package-registry", nil)["port"]; got != float64(3000) {
|
|
t.Errorf("without a setting the forge serves %v, not the catalogue's port", got)
|
|
}
|
|
// And so is where a repository on it is cloned from (novox/hq ADR 0111), for the same reason:
|
|
// a build composes the URL from what the forge serves, so a given port is a followed port.
|
|
if got := ServedOn(forge, "git", given)["port"]; got != 3100 {
|
|
t.Errorf("git is served on %v, not the port this node gave the forge", got)
|
|
}
|
|
}
|
|
|
|
// **The builder requires the registry the npm seat delivers, and carries no binding of its own.**
|
|
//
|
|
// It used to carry a hand-written binding because nothing provided a package registry to resolve
|
|
// one from at genesis. The catalogue now requires it like any consumer, and ADR 0110 makes the
|
|
// seat's holder the answer when more than one module provides it — so a carried copy would be a
|
|
// second answer to the same question, free to drift from the first. Asserted gone, not merely
|
|
// unused.
|
|
func TestTheBuilderRequiresTheRegistryTheNpmSeatDelivers(t *testing.T) {
|
|
builder := catalogueManifest(t, "builder")
|
|
seat, _ := SeatNamed("npm-package-registry")
|
|
var requires bool
|
|
for _, r := range builder.Requires {
|
|
requires = requires || r == seat.Delivers
|
|
}
|
|
if !requires {
|
|
t.Fatalf("the builder does not require %q: %v", seat.Delivers, builder.Requires)
|
|
}
|
|
if builder.Binds[seat.Delivers] == "" {
|
|
t.Errorf("the builder is not told where the registry is: binds %v", builder.Binds)
|
|
}
|
|
for _, r := range builder.Resources {
|
|
if fmt.Sprint(r["id"]) == "package-binding" {
|
|
t.Fatal("the builder carries its own package binding beside the one the mesh resolves")
|
|
}
|
|
}
|
|
}
|
|
|
|
// The forge holds the seats it answers for (novox/hq ADR 0110, 0111), parsed by the real parser —
|
|
// which refuses a delivering seat claimed by a module that does not provide what it delivers.
|
|
func TestTheForgeHoldsTheNpmAndGitSeats(t *testing.T) {
|
|
forge := catalogueManifest(t, "gitea")
|
|
holds := map[string]bool{}
|
|
for _, c := range forge.Claims {
|
|
holds[c.Name] = true
|
|
}
|
|
for _, seat := range []string{"npm-package-registry", "git"} {
|
|
if !holds[seat] {
|
|
t.Errorf("gitea does not claim the %s seat: %+v", seat, forge.Claims)
|
|
}
|
|
}
|
|
git := ServedOn(forge, "git", nil)
|
|
if git["scheme"] != "http" || git["port"] != float64(3000) {
|
|
t.Errorf("gitea serves nothing a clone URL can be composed from: %v", git)
|
|
}
|
|
npm := ServedOn(forge, "npm-package-registry", nil)
|
|
if npm["npm-path"] != "/api/packages/novox/npm/" {
|
|
t.Errorf("gitea no longer says where its npm registry is: %v", npm)
|
|
}
|
|
}
|
|
|
|
// **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"])
|
|
}
|
|
}
|
|
|
|
// gitea's own sshd is unmodified — the module's own internal port is 22, the number in
|
|
// `listens`, the same convention every other module in the catalogue uses (its internal port,
|
|
// not an invented identity). Composed from the manifest in the catalogue beside this checkout,
|
|
// because what the mesh publishes is a fact about what the module actually writes.
|
|
func declaredGiteaSsh(t *testing.T, given map[int]int) map[string]any {
|
|
t.Helper()
|
|
forge := catalogueManifest(t, "gitea")
|
|
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"},
|
|
}}
|
|
givenPorts := map[int]int{3000: 3000}
|
|
for k, v := range given {
|
|
givenPorts[k] = v
|
|
}
|
|
out, err := r.Declaration(Rendering{
|
|
Needed: map[string]map[string]string{"gitea": {"broker": "sealed-broker"}},
|
|
Ports: map[string]map[int]int{"gitea": givenPorts},
|
|
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)
|
|
}
|
|
return server
|
|
}
|
|
|
|
// **The forge publishes ssh at the mesh's own fixed convention by default** (novox/hq ADR 0100).
|
|
//
|
|
// `222` is the mesh's own public convention for the forge's ssh, written directly in the
|
|
// manifest's `ports` — every node the forge has run on used the same number, so it needs no
|
|
// per-node setting to reach it.
|
|
func TestTheForgesSshPortIsTheMeshsFixedConventionByDefault(t *testing.T) {
|
|
forge := catalogueManifest(t, "gitea")
|
|
// Nothing was given — no node moved this port — which is the ordinary answer: the mesh only
|
|
// reports what a setting moved, and the manifest's own `222:22` needs no move to be reached.
|
|
given, err := GivenPorts(forge, nil)
|
|
if err != nil {
|
|
t.Fatalf("the forge's ssh port cannot be given on a node: %v", err)
|
|
}
|
|
if len(given) != 0 {
|
|
t.Fatalf("nothing moved the forge's ssh port, yet it was given %v", given)
|
|
}
|
|
server := declaredGiteaSsh(t, given)
|
|
if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "222:22") {
|
|
t.Fatalf("the forge is published on %v, not its own fixed convention", server["ports"])
|
|
}
|
|
}
|
|
|
|
// **A node whose predecessor served git on a different number can still be told to leave it
|
|
// there.** The setting names the port the module itself listens on — 22, gitea's own sshd, the
|
|
// same number `listens` uses — not the mesh's own default machine-side number, so moving it does
|
|
// not require guessing what the manifest happens to default to.
|
|
func TestANodeMayGiveTheForgesSshPortADifferentNumber(t *testing.T) {
|
|
forge := catalogueManifest(t, "gitea")
|
|
given, err := GivenPorts(forge, []Layer{{From: "anchor",
|
|
Values: map[string]any{PortsSetting: map[string]any{"22": float64(9022)}}}})
|
|
if err != nil {
|
|
t.Fatalf("the forge's ssh port cannot be moved on a node: %v", err)
|
|
}
|
|
if want := map[int]int{22: 9022}; !reflect.DeepEqual(given, want) {
|
|
t.Fatalf("the forge was given %v, and the setting named %v", given, want)
|
|
}
|
|
server := declaredGiteaSsh(t, given)
|
|
if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "9022:22") ||
|
|
strings.Contains(published, "222:22") {
|
|
t.Fatalf("the forge is published on %v, not the port this node gave it", server["ports"])
|
|
}
|
|
}
|