The package registry was the one foundation port not resolved from what its module serves. Nothing in the controller had to change for it — `ports` on the forge moves its container, what it serves and what consumers are told, and the builder's carried binding is settable like any other mergeable file — but nothing said so, which is how it came to be special in the first place. Two tests over the catalogue's own manifests: the forge's port is given on a node and reaches what it serves, and the builder's carried binding takes the port from the node while keeping who the binding is with. novox/hq 04-ISSUES/085
181 lines
7.2 KiB
Go
181 lines
7.2 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)
|
|
}
|
|
}
|
|
|
|
func TestTheBuildersPackageBindingKeepsItsIdentity(t *testing.T) {
|
|
builder := catalogueManifest(t, "builder")
|
|
for _, key := range []string{"provision", "from", "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)
|
|
}
|
|
}
|
|
}
|