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) } } }