The third review of #170 (hq issue 339): a setting overrides any key a provider serves, so any caller of the settings verb could move a database's port, a registry's port or an issuer to a listener of its own and collect what consumers present. TerminalKeys now derives from the manifest: places, accesses, every served key and every setting a served value asks for, and every setting a file marked `trusted` asks for. `trusted` is the catalogue's word, taken out before the declaration; `module check` warns of a file that asks for a setting without saying, and refuses it from 2026-10-30. The hand list is gone. A directory used as found is now its own condition kind, the operator's, never urgent, and the gate exempts it where it exempts a relogin.
157 lines
6.9 KiB
Go
157 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A directory the node-engine uses as found (novox/hq issue 339) waits for a person to hand it over at the
|
|
// machine. Found before this send, it is no fault of the build: the gate passes with the wait carried, so an
|
|
// urgent fix of that module still goes through. Found by this send, the send brought it, and the gate holds.
|
|
func foundDirectory(module string, since time.Time) inventory.ResourceHealth {
|
|
return inventory.ResourceHealth{Module: module, Resource: module + ".data", Kind: link.KindDirectory,
|
|
Target: "/srv/" + module, State: link.StateUnhealthy, Since: since,
|
|
Reason: link.ReasonUsedAsFound + " owned by 1000:1000, mode 700, as found; root, mode 755 was declared and " +
|
|
"not given it — `mesh-host hand-over` at the machine hands it to the mesh"}
|
|
}
|
|
|
|
func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) {
|
|
now := time.Now()
|
|
sent := now.Add(-time.Minute)
|
|
f := gateFacts{now: now, health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: now,
|
|
Resources: []inventory.ResourceHealth{foundDirectory("notes", sent.Add(-24*time.Hour))}}}}
|
|
h, why := moduleHealthWord("notes", "laptop", sent, f)
|
|
if h != healthPerson || !strings.Contains(why, "notes.data") || !strings.Contains(why, "hand-over") {
|
|
t.Fatalf("a directory found before the send reads %v %q; want a wait for a person", h, why)
|
|
}
|
|
// Found by this very send: the send brought it, and it is not passed.
|
|
f.health["laptop"] = inventory.NodeHealth{Node: "laptop", HeardAt: now,
|
|
Resources: []inventory.ResourceHealth{foundDirectory("notes", sent.Add(time.Second))}}
|
|
if h, why := moduleHealthWord("notes", "laptop", sent, f); h != healthNotYet {
|
|
t.Fatalf("a directory this send found reads %v %q; want not yet", h, why)
|
|
}
|
|
// A container down beside the old wait is a fault, as before.
|
|
f.health["laptop"] = inventory.NodeHealth{Node: "laptop", HeardAt: now, Resources: []inventory.ResourceHealth{
|
|
foundDirectory("notes", sent.Add(-time.Hour)),
|
|
{Module: "notes", Resource: "notes.web", Kind: "container", Target: "notes", State: link.StateUnhealthy, Reason: "down"}}}
|
|
if h, why := moduleHealthWord("notes", "laptop", sent, f); h != healthNotYet {
|
|
t.Fatalf("a container down beside the wait reads %v %q; want not yet", h, why)
|
|
}
|
|
}
|
|
|
|
// The whole walk: a module whose directory was used as found long before still gets its fix to every machine,
|
|
// its pass kept and the wait said; a directory this very send found holds it and puts it back.
|
|
func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
found time.Duration // when the directory was found, against now
|
|
passes bool
|
|
}{
|
|
{"found a day before the send", -24 * time.Hour, true},
|
|
{"found by this send", time.Hour, false},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
b := aBacklog(t)
|
|
ctx := t.Context()
|
|
inv := b.open.inventory
|
|
releaseHeard = func(context.Context, *stores) (map[string]bool, error) {
|
|
return map[string]bool{"anchor": true, "laptop": true}, nil
|
|
}
|
|
backlogFacts := gatherGateFacts
|
|
gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) {
|
|
f, err := backlogFacts(ctx, open, component)
|
|
// The used-as-found condition, raised after the send (its second statement): its own kind, never a
|
|
// fault the gate reads as the build's.
|
|
f.judged, f.openErr = true, nil
|
|
f.open = append(f.open, conditions.Condition{Key: usedAsFoundKey("app", "anchor"), Kind: kindUsedAsFound,
|
|
Subject: conditions.Subject{Scope: conditions.ScopeModule, ID: "app.anchor", Machine: "anchor"},
|
|
Raised: time.Now()})
|
|
f.health = map[string]inventory.NodeHealth{}
|
|
for _, n := range []string{"anchor", "laptop"} {
|
|
f.health[n] = inventory.NodeHealth{Node: n, HeardAt: time.Now(), Resources: []inventory.ResourceHealth{
|
|
{Module: "app", Resource: "app.web", Kind: "container", Target: "app", State: link.StateHealthy},
|
|
foundDirectory("app", time.Now().Add(c.found)),
|
|
{Module: "late", Resource: "late.web", Kind: "container", Target: "late", State: link.StateHealthy}}}
|
|
}
|
|
return f, err
|
|
}
|
|
wasSettle, wasEvery, wasBound := gateSettle, gateEvery, gateBound
|
|
t.Cleanup(func() { gateSettle, gateEvery, gateBound = wasSettle, wasEvery, wasBound })
|
|
gateSettle, gateEvery, gateBound = 0, 0, 300*time.Millisecond
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
advancePlans(ctx, b.open)
|
|
if p := b.release(t); p.State != inventory.PlanRolling {
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
p := b.release(t)
|
|
v, found, err := inv.GateOf(ctx, "build-app-c2")
|
|
if c.passes {
|
|
if p.State != inventory.PlanDone || err != nil || !found || v.Verdict != inventory.GatePassed {
|
|
t.Fatalf("the walk is %s (%s); app's verdict %+v: want the fix through", p.State, p.Note, v)
|
|
}
|
|
if !strings.Contains(v.Why+p.Note, "hand it over") {
|
|
t.Errorf("the wait is not carried: verdict %q, walk %q", v.Why, p.Note)
|
|
}
|
|
return
|
|
}
|
|
if p.State == inventory.PlanDone {
|
|
t.Fatalf("a directory this send found let the walk through: %s", p.Note)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The condition says the wait in its own kind: the operator's, never urgent, and cleared once handed over.
|
|
func TestADirectoryUsedAsFoundIsItsOwnCondition(t *testing.T) {
|
|
k, _ := withConditionsInMemory(t)
|
|
ctx := t.Context()
|
|
rs := map[string][]inventory.ResourceHealth{"notes": {foundDirectory("notes", time.Now().Add(-time.Hour))}}
|
|
for i := 0; i < 2; i++ {
|
|
if err := judgeModuleHealth(ctx, nil, k, "laptop", rs, map[string]int{"notes": i + 1}, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
open, _ := k.Open(ctx)
|
|
var got *conditions.Condition
|
|
for i, c := range open {
|
|
if c.Key == usedAsFoundKey("notes", "laptop") {
|
|
got = &open[i]
|
|
}
|
|
if c.Kind == kindModuleUnhealthy {
|
|
t.Fatalf("raised as a fault: %+v", c)
|
|
}
|
|
}
|
|
if got == nil || got.Resolver != conditions.ResolverOperator || got.Severity == conditions.Urgent ||
|
|
!strings.Contains(got.Summary, "notes.data") {
|
|
t.Fatalf("the condition: %+v", got)
|
|
}
|
|
// Long open is still not urgent: only a person can hand it over, and nothing is broken by the wait.
|
|
if err := judgeModuleHealth(ctx, nil, k, "laptop", rs, map[string]int{"notes": 3}, time.Now().Add(48*time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
open, _ = k.Open(ctx)
|
|
for _, c := range open {
|
|
if c.Key == usedAsFoundKey("notes", "laptop") && c.Severity == conditions.Urgent {
|
|
t.Fatal("a directory used as found became urgent")
|
|
}
|
|
}
|
|
if err := judgeModuleHealth(ctx, nil, k, "laptop", map[string][]inventory.ResourceHealth{}, nil, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
open, _ = k.Open(ctx)
|
|
for _, c := range open {
|
|
if c.Key == usedAsFoundKey("notes", "laptop") {
|
|
t.Fatal("not cleared once handed over")
|
|
}
|
|
}
|
|
}
|