106 lines
4.8 KiB
Go
106 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq ADR 0100: every declaration an adopted node is sent says it is adopted and which modules
|
|
// were taken on it; a converged node's declaration is byte for byte what it was.
|
|
|
|
// helloWeb is a module the predecessor also runs: a page and a server under names it uses.
|
|
func helloWeb() catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "hello-web", Version: "1",
|
|
Resources: []map[string]any{
|
|
{"id": "page", "type": "file", "path": "/var/lib/hello-web/index.html", "content": "hello"},
|
|
{"id": "server", "type": "container", "name": "hello-web",
|
|
"image": "registry.example/hello@sha256:" + strings.Repeat("a", 64)},
|
|
{"id": "served", "type": "directory", "path": "/var/lib/hello-web"},
|
|
}}
|
|
}
|
|
|
|
// composed is what node would be sent now, as push composes it.
|
|
func composed(t *testing.T, open *stores, node string) sendable {
|
|
t.Helper()
|
|
plan, settings, err := planFor(t.Context(), open, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared, err := declarationFor(t.Context(), open, node, plan, settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return declared
|
|
}
|
|
|
|
// convergedBefore is the envelope a converged node was sent before adoption existed, captured by
|
|
// running this same composition at the commit this branch left main (0a39b7d). The mesh here is
|
|
// aMesh's laptop with the private network taken off it, so nothing in the declaration is random:
|
|
// what changes this string is a change to what a converged machine is sent, which is the thing an
|
|
// older host would refuse.
|
|
//
|
|
// Re-captured 2026-10-01 (novox/hq issue 177): c978aa7 took `hosts` off every container — a
|
|
// machine's own resolver knows the mesh's names now — and left this string carrying it, so the
|
|
// guard failed for a day and nothing ran it. A field an older host never sees is the one change
|
|
// this guard permits; a field it would refuse is the one it exists to catch.
|
|
const convergedBefore = `{"declaration":1,"resources":[{"content":"hello","id":"hello-web.page","path":"/var/lib/hello-web/index.html","type":"file"},{"id":"hello-web.server","image":"registry.example/hello@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","name":"hello-web","type":"container"},{"id":"hello-web.served","path":"/var/lib/hello-web","type":"directory"}]}`
|
|
|
|
func mustNodes(t *testing.T, open *stores) []inventory.Node {
|
|
t.Helper()
|
|
nodes, err := open.inventory.Nodes(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// stdoutOf is what run printed.
|
|
|
|
// novox/hq ADR 0100: a port a node was given for the store is where its consumers on other
|
|
// machines are told to reach it, and a port given for the whole mesh is refused.
|
|
|
|
// novox/hq ADR 0103: the guard an adopted node is sent follows what was taken there. A store
|
|
// assigned but not taken is not guarded — its port may still be the predecessor's — and taking it
|
|
// guards it from the next declaration.
|
|
|
|
// novox/hq ADR 0038 and 0100: a module may publish a port the long way — `2222:22`, because the
|
|
// machine's own ssh daemon holds 22 — and say it listens on the machine side of that mapping,
|
|
// which is the number anything reaching it dials. A node moves that port by naming it, and the
|
|
// number has to reach everything derived from it at once: what the runtime is handed, what an
|
|
// adopted node is told to open, what its guard refuses, and what a consumer elsewhere dials.
|
|
//
|
|
// It reached none of them. The setting was refused outright for naming the machine side — so a
|
|
// module's port could not be put back where the machine it replaces had it, and, worse, the
|
|
// node's every push failed for as long as the setting existed.
|
|
|
|
func TestAnEmptyDeclarationSaysOwnsNothing(t *testing.T) {
|
|
// The host refuses an empty body unless told the emptiness is meant (novox/hq issue 127).
|
|
body, err := sendable{}.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env map[string]any
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if env["owns_nothing"] != true {
|
|
t.Fatalf("an empty declaration must mark owns_nothing; got %v", env)
|
|
}
|
|
// A declaration with resources does not carry the marker.
|
|
body, _ = sendable{Resources: []map[string]any{{"id": "x"}}}.Body()
|
|
var env2 map[string]any
|
|
_ = json.Unmarshal(body, &env2)
|
|
if _, present := env2["owns_nothing"]; present {
|
|
t.Fatalf("a non-empty declaration must not mark owns_nothing; got %v", env)
|
|
}
|
|
}
|
|
|
|
// A setting is judged where it is stored, and an impossible one costs a module, not a machine
|
|
// (novox/hq ADR 0163, rule 6): stored while it composed, a setting whose definition then moved from
|
|
// under it leaves that module out of the declaration — said in the envelope, so the host keeps the
|
|
// module's things — and the machine is told everything else.
|