${port:…} answered only inside a file's content, and the one place a module
routinely writes its own address is a container's `env` — where the literal is
wrong on every node whose assignment differs from the manifest's number, and
wrong again on a node given that port as a setting (ADR 0100). Nothing checked
it: the value is a string like any other, and it fails at runtime, on one node.
Filled by the control plane, like a bound value: a port is not secret, so there
is nothing for the host to be the only witness of and it learns no new field.
That is the line ADR 0086 draws — its objection is to a secret being in an
environment at all, not to who fills one in — so a port crosses it and a
credential still does not. Same guard as before: a port the module never said it
listens on is refused, now naming the container and the variable.
The env map is the catalogue's, shared by every node running the module, and the
resource around it is a shallow copy, so a filled value goes into a fresh map —
otherwise the first node composed writes its own port into the manifest and
every node after it is told that one.
Inert on the catalogue as it stands: ${port:…} is written in one other place in
it, a file. Renamed off _files, which this no longer is.
253 lines
11 KiB
Go
253 lines
11 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"])
|
|
}
|
|
}
|