Every one of the 48 core failures of research 031 was found by a person looking; the mesh's answers carried the fact for whoever asked and told nobody. - The condition store (to-be 45 §2): mesh-controller_conditions, one key per open condition, written by compare-and-set so a person's silence and the watchdogs never lose each other's word; every transition kept ninety days in mesh-controller_condition-history and said as the seat's events condition-raised / condition-changed / condition-cleared (the condition at the top level, with event, at, change, why, show), offered again while the bus is away. Raised and cleared by observation only; a clearing reopened within ten minutes is the same condition with its count up, its silence kept. Verbs: conditions, conditions show, conditions silence (a hand act, at most a week), conditions history. - ADR 0224's provider standing is the first kind, provider-failing, held by the provider's events; the provider_standing table is no longer read or written (left in place: dropping it is the operator's word). - status leads with the open conditions, urgent first, and says all well only with none open; conditions it cannot read are said and not well. - The signals table compiled in, one watchdog loop over it every 30s: S1 heartbeat (3 intervals, asleep machines excepted, control node urgent after 30 min), S2 report after a send, S3 plan tier, S4 event loop deaf, S5 merge not acted, S6 ask lost, S7 call hung, S8 provider silent, S9 advisories, S10 self-check silent, S11 node tools silent, S13 stale refusals; S12, S14, S15 deferred with their reasons. A row that cannot see raises probe-failed and clears nothing. A test generated from the table suppresses each signal inside and past its bound. - The bus's advisories (maximum deliveries, a mesh consumer deleted) and the controller's own slow consumer and refused subjects, said in the mesh's words. - doctor: the probe registry D1-D10 (D5 deferred) and DW, every five minutes, each in thirty seconds; a probe that cannot run is never a pass. D1 validates with mesh-host's own validator. Every run ends with the doctor-heartbeat event mesh-watcher listens for. - The controller is granted its new buckets, events, the two advisories and $SRV.INFO; the node tools their tools-alive heartbeat. The streams and consumers the controller asserts and the ones D6/D7 expect are one derivation.
129 lines
4.4 KiB
Go
129 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/licences"
|
|
"github.com/novox/mesh-controller/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)
|
|
// A condition store of its own, held in memory (novox/hq to-be 45 §2): status leads with what is
|
|
// open, and a mesh with no bus would otherwise read as one whose conditions cannot be read.
|
|
withConditionsInMemory(t)
|
|
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}
|
|
}
|
|
|
|
// withConditionsInMemory gives the test a condition store in memory, as the serving controller's.
|
|
func withConditionsInMemory(t *testing.T) (*conditions.Keeper, *conditions.InMemory) {
|
|
t.Helper()
|
|
store := conditions.NewInMemory()
|
|
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store})
|
|
before := conditionsFrom
|
|
conditionsFrom = k
|
|
t.Cleanup(func() {
|
|
conditionsFrom = before
|
|
k.Close(context.Background())
|
|
})
|
|
return k, store
|
|
}
|