An assignment was verified by resolving the node it was made on. The verify that matters is resolution over all of them: a module offering a mesh-scoped provision stops offering it the moment its own node stops resolving, so an assignment could be reported as fine while it took that provision away from every consumer elsewhere. Those consumers were then told "nothing in this mesh provides it", naming as the remedy a module that was already assigned — a wrong answer about a machine nobody had touched. That is novox/hq 04-ISSUES/017's shape exactly: an action succeeds into a state its own verify rejects, and it does so because the action's own test is not the test the verify uses. 017's remedy was to make them the same test, and this makes them the same test. The assignment is still kept, and that is the other half of the decision. Assignment is not an ordering: a consumer assigned before its provider does not resolve for as long as it takes to assign the provider, and refusing the first half of a pair would make the order somebody types two commands in part of the mesh's rules. So `assign` and `unassign` now name every OTHER machine that cannot be worked out as things stand, in the mesh's own words, beside whatever they already said about this one. It reports the state and never claims causation — saying "this assignment broke laptop" would mean resolving the whole mesh twice and would still be a guess about which of several changes did it. It costs a resolution per machine. Assignment is a person typing a command, and being told which machines this just blocked is worth more than the milliseconds. This is what a four-node raise read as "bumping a module's version broke provider recognition". It was neither the version nor the provider: nothing in this codebase reads the version column, every lookup is keyed on the module name alone, and a test in the previous commit now says so. It was one machine's set of assignments, and nothing said so. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
"github.com/novox/mesh-control/internal/licences"
|
|
"github.com/novox/mesh-control/internal/overlay"
|
|
)
|
|
|
|
// A mesh a command can be run against.
|
|
//
|
|
// The commands here were tested through the pieces they call and never through themselves, so
|
|
// three faults that only exist where the pieces meet — an assignment reported as fine while it
|
|
// blocked other machines, a read-shaped invocation that wrote, a JSON interface that stopped
|
|
// emitting JSON — were invisible to every test in this package. This raises the real stores and
|
|
// calls the real functions.
|
|
|
|
// aMesh is two placed, capable machines on a private network, with nothing assigned but the
|
|
// network itself.
|
|
//
|
|
// `anchor` is the hub. That is not decoration: a mesh whose hub cannot be resolved has no private
|
|
// network at all, which is how one machine's problem reaches every other.
|
|
func aMesh(t *testing.T) *stores {
|
|
t.Helper()
|
|
inventory.ForTest(t) // raises the store, migrates it, and points the environment at it
|
|
licences.ForTest(t) // planning reaches this one too, by name and never by connection
|
|
|
|
open, err := openStores(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(open.Close)
|
|
for _, m := range provided {
|
|
if err := open.inventory.Provide(t.Context(), m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
for i, name := range []string{"anchor", "laptop"} {
|
|
record, err := open.inventory.AddNode(t.Context(), name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.SetPlace(t.Context(), name, name+".example:51820", "here",
|
|
name == "anchor", fmt.Sprintf("10.77.0.%d", i+1)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reported, err := json.Marshal(map[string]any{"capabilities": []map[string]any{
|
|
{"name": "container-runtime", "present": true},
|
|
{"name": "wireguard", "present": true},
|
|
{"name": "systemd", "present": true},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var profile map[string]any
|
|
if err := json.Unmarshal(reported, &profile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.RecordProfile(t.Context(), record.ID, profile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.RecordSealingKey(t.Context(), record.ID, aPublicKey(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.RecordOverlayKey(t.Context(), record.ID, aPublicKey(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.Assign(t.Context(), name, overlay.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return open
|
|
}
|
|
|
|
// aPublicKey is a key a machine could have reported. Its private half is thrown away: nothing here
|
|
// opens anything, it only needs the mesh to believe a machine has a key.
|
|
func aPublicKey(t *testing.T) string {
|
|
t.Helper()
|
|
k, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes())
|
|
}
|
|
|
|
// register puts a manifest in the catalogue.
|
|
func register(t *testing.T, open *stores, m catalogue.Manifest) {
|
|
t.Helper()
|
|
if err := open.inventory.RegisterModule(t.Context(), m, inventory.Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// rivals are two modules that cannot share a machine, which is the shortest way to make a node's
|
|
// own set of assignments incoherent using nothing but commands a person has.
|
|
func rivals() (catalogue.Manifest, catalogue.Manifest) {
|
|
claim := []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}
|
|
return catalogue.Manifest{Module: "rival-one", Version: "1", Claims: claim},
|
|
catalogue.Manifest{Module: "rival-two", Version: "1", Claims: claim}
|
|
}
|