TestTheForgesSshPortIsGivenByTheNumberTheForgeCallsIt exercised a settings override from '2222' to 222 — but 2222 was never a real port anywhere, just a mistake in gitea's own manifest (fixed alongside this: listens.port is now 22, the container's real internal sshd port, matching every other module's convention, and ports declares 222:22 directly — 222 has always been the real, fixed public git-ssh port, needing no per-node override). Split into two tests: the fixed default with no override, and a genuine override case for a hypothetical node whose predecessor used a different number, keyed correctly by 22. TestTheResolverAndWhatAsksItComposeOnOneMachine set Rendering.Names but FactNodeZones reads Rendering.Machines (novox/hq issue 111 split the two apart: every name the mesh serves vs. the machines subset) — a loose end from that merge, not exercised until now. Both are the same map in this test's scenario, so both fields are set.
336 lines
15 KiB
Go
336 lines
15 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"])
|
|
}
|
|
}
|
|
|
|
// 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"])
|
|
}
|
|
}
|