The half of the module system that was built and never proved. Unassigning i3 removed i3's file AND xorg's, because xorg was only there to satisfy i3 -- the node's own record agrees, and the resolution the mesh sends no longer mentions either. That works because a declaration removes what the mesh previously declared and nothing else, which is 04-ISSUES/010's fix carrying its weight here: the substrate the machine raised for itself is untouched by any of it. Tests for the storage layer, which had none. The ones worth naming: A module a machine is running cannot be forgotten -- not a fault, it means the mesh would lose the ability to describe what is on that machine. Removing a node DOES take its assignments, and the asymmetry is deliberate: a node that is gone cannot be running anything. A node that has never reported has NO capabilities rather than all of them. That refuses anything needing one, which is wrong but visible -- where assuming it can do everything would assign work it cannot do and find out on the machine. And a capability the node reported as ABSENT is not counted: reading the list without the verdict would let a module onto a machine that said no. `overlay push` is gone, replaced by `push`, which sends a node its network and its modules as one declaration. Two commands that overlap is how a mesh ends up half-configured by whichever was run. The old name answers with where to go, and answers before opening a database -- needing one would turn a redirect into a connection error.
213 lines
6.6 KiB
Go
213 lines
6.6 KiB
Go
package inventory
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
)
|
|
|
|
func manifest(name string, provides, requires []string) catalogue.Manifest {
|
|
return catalogue.Manifest{Module: name, Provides: provides, Requires: requires}
|
|
}
|
|
|
|
func TestAModuleRoundTripsWholeAndUnshredded(t *testing.T) {
|
|
// The manifest is held as it was given. Every field of it is read together when a node is
|
|
// resolved, and a manifest that gains a field should not need a migration before it can be
|
|
// stored — the module system is the thing most likely to grow.
|
|
inv := fresh(t)
|
|
m := catalogue.Manifest{
|
|
Module: "xorg", Provides: []string{"display-server"},
|
|
Capabilities: []string{"seat"},
|
|
Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}},
|
|
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/X11/x.conf"}},
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, ok := shelf["xorg"]
|
|
if !ok {
|
|
t.Fatal("the module was not in the catalogue")
|
|
}
|
|
if len(back.Claims) != 1 || back.Claims[0].Name != "the-seat" {
|
|
t.Errorf("the claims did not survive: %+v", back.Claims)
|
|
}
|
|
if len(back.Resources) != 1 || back.Resources[0]["path"] != "/etc/X11/x.conf" {
|
|
t.Errorf("the resources did not survive: %+v", back.Resources)
|
|
}
|
|
}
|
|
|
|
func TestRegisteringAgainReplacesTheManifest(t *testing.T) {
|
|
// A manifest changing is the ordinary case — a module gains a requirement, a claim, a
|
|
// resource. What matters is that the change is what the next resolution sees.
|
|
inv := fresh(t)
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(shelf) != 1 {
|
|
t.Fatalf("registering twice made %d modules", len(shelf))
|
|
}
|
|
if len(shelf["thing"].Provides) != 1 {
|
|
t.Error("the second manifest did not replace the first")
|
|
}
|
|
}
|
|
|
|
func TestAModuleAMachineIsRunningCannotBeForgotten(t *testing.T) {
|
|
// Not a fault. It means a machine is running that module now, and removing the record would
|
|
// leave the mesh unable to describe what is on it.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := inv.ForgetModule(t.Context(), "thing")
|
|
if !errors.Is(err, ErrStillAssigned) {
|
|
t.Fatalf("a module in use was forgotten: %v", err)
|
|
}
|
|
if err := inv.Unassign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.ForgetModule(t.Context(), "thing"); err != nil {
|
|
t.Errorf("an unassigned module could not be forgotten: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemovingANodeTakesItsAssignments(t *testing.T) {
|
|
// The asymmetry with modules above, and it is deliberate: a node that is gone cannot be
|
|
// running anything, so its assignments are meaningless rather than dangerous.
|
|
inv := fresh(t)
|
|
node, err := inv.AddNode(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.store.Pool().Exec(t.Context(), `delete from node where id = $1`, node.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var left int
|
|
if err := inv.store.Pool().QueryRow(t.Context(),
|
|
`select count(*) from assignment`).Scan(&left); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if left != 0 {
|
|
t.Errorf("%d assignment(s) outlived the node they were on", left)
|
|
}
|
|
// And the module itself survives, because other nodes may be running it.
|
|
shelf, err := inv.Catalogue(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(shelf) != 1 {
|
|
t.Error("removing a node took a module with it")
|
|
}
|
|
}
|
|
|
|
func TestAssigningAModuleTheMeshDoesNotKnowIsRefused(t *testing.T) {
|
|
// Said as "no module of that name" rather than as a foreign key. A person mistyping a module
|
|
// name should be told that, not shown a constraint.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := inv.Assign(t.Context(), "laptop", "not-a-module")
|
|
if !errors.Is(err, ErrNoSuchModule) {
|
|
t.Fatalf("assigning an unknown module gave %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAssigningTwiceIsNotAnError(t *testing.T) {
|
|
// It is a statement of what should be true, and it already is.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
|
t.Fatalf("assigning again failed: %v", err)
|
|
}
|
|
}
|
|
assigned, err := inv.Assigned(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(assigned) != 1 {
|
|
t.Errorf("assigned three times and got %v", assigned)
|
|
}
|
|
}
|
|
|
|
func TestANodeThatNeverReportedHasNoCapabilities(t *testing.T) {
|
|
// Not "everything". A node that has never spoken will refuse anything needing a capability,
|
|
// which is wrong but visible — where assuming it can do everything would assign work it
|
|
// cannot do and find out on the machine.
|
|
inv := fresh(t)
|
|
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caps, err := inv.ProfileOf(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caps) != 0 {
|
|
t.Errorf("a node that never reported has capabilities: %v", caps)
|
|
}
|
|
}
|
|
|
|
func TestOnlyPresentCapabilitiesCount(t *testing.T) {
|
|
// A profile lists what was looked for and whether it was found. A capability that was looked
|
|
// for and absent is the same as one nobody looked for, as far as what may run here goes —
|
|
// and reading the list without the verdict would let a module onto a machine that reported
|
|
// "no".
|
|
inv := fresh(t)
|
|
node, err := inv.AddNode(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordProfile(t.Context(), node.ID, map[string]any{
|
|
"capabilities": []any{
|
|
map[string]any{"name": "seat", "present": true},
|
|
map[string]any{"name": "firewall", "present": false},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
caps, err := inv.ProfileOf(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !caps["seat"] {
|
|
t.Error("a capability the node reported as present is missing")
|
|
}
|
|
if caps["firewall"] {
|
|
t.Error("a capability the node reported as ABSENT was counted as present")
|
|
}
|
|
}
|