Hold consumers under a provider waiting for the operator, and say a wait beside another condition (issue 405)
A provider whose only part not healthy waits for the operator's secret or setting let its consumers raise their own unhealthy conditions, and a wait beside a relogin, a directory used as found or a fault was not said until the other cleared.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// A provider waiting for the operator holds its consumers, and a wait beside another wait is said (novox/hq
|
||||
// issue 405, found in the review of ADR 0283's controller change).
|
||||
//
|
||||
// The shapes are those of issue 386 (mounts waiting for smb-password-games: waitingResource, passwordWait) and of
|
||||
// the openrazer wait on the workstation of 2026-10-11 (relogin, userUnit): an account in its group whose session
|
||||
// began before it was, and its unit failed in that account's own service manager.
|
||||
|
||||
// waitingDatabase is a provider whose only part not healthy waits for the operator's secret: a database's
|
||||
// process stated waiting, as the node-engine states a tool check answering waits (ADR 0283 decision 2).
|
||||
func waitingDatabase() inventory.ResourceHealth {
|
||||
return inventory.ResourceHealth{Module: "db", Resource: "db.server", Kind: "process", Target: "db.service",
|
||||
State: link.StateWaiting, Check: "tool", Reason: "the database waits for its licence",
|
||||
Waits: []inventory.Wait{{Part: "the server", Secret: "licence", What: "the licence key of the database"}}}
|
||||
}
|
||||
|
||||
// failingConsumer is a consumer whose check that needs the database fails.
|
||||
func failingConsumer(module string) inventory.ResourceHealth {
|
||||
return inventory.ResourceHealth{Module: module, Resource: module + ".web", Kind: "container", Target: module,
|
||||
State: link.StateUnhealthy, Reason: "http /health on web: answered 500", Check: "http", Needs: "postgres-database"}
|
||||
}
|
||||
|
||||
// holdingOf is one reading of the record without a store: the newest statements, the open conditions, and each
|
||||
// consumer's provider already looked up, as providerFor memoises it.
|
||||
func holdingOf(open []conditions.Condition, healths map[string]inventory.NodeHealth, bound map[[3]string]catalogue.Chosen) *holding {
|
||||
h := &holding{ctx: context.Background(), healths: healths, open: open, providers: map[string]providerLookup{}}
|
||||
for k, p := range bound {
|
||||
h.providers[k[0]+"\x00"+k[1]+"\x00"+k[2]] = providerLookup{p, true}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
var theDatabase = catalogue.Chosen{Node: "anchor", Module: "db"}
|
||||
|
||||
// (1) A provider whose only part not healthy waits for the operator holds the findings of its consumers under its
|
||||
// needs-operator condition, as ADR 0240 rule 5 holds them under an unhealthy one.
|
||||
func TestAProviderWaitingForTheOperatorHoldsItsConsumers(t *testing.T) {
|
||||
healths := map[string]inventory.NodeHealth{
|
||||
"anchor": {Node: "anchor", Resources: []inventory.ResourceHealth{waitingDatabase()}},
|
||||
"laptop": {Node: "laptop", Resources: []inventory.ResourceHealth{failingConsumer("shop")}},
|
||||
}
|
||||
bound := map[[3]string]catalogue.Chosen{{"laptop", "shop", "postgres-database"}: theDatabase}
|
||||
hold := holdingOf(nil, healths, bound)
|
||||
p, held := hold.heldUnder("laptop", "shop", []inventory.ResourceHealth{failingConsumer("shop")})
|
||||
if !held || p != theDatabase {
|
||||
t.Fatalf("a consumer of a provider waiting for the operator is not held under it: %v %v", p, held)
|
||||
}
|
||||
if got := hold.waitersOn(theDatabase); len(got) != 1 || got[0] != "shop on laptop" {
|
||||
t.Fatalf("the waiting provider lists %v as waiting on it; want shop on laptop", got)
|
||||
}
|
||||
// Its needs-operator condition open is enough, as its unhealthy one is.
|
||||
open := []conditions.Condition{{Key: needsOperatorKey("db", "anchor"), Kind: kindNeedsOperator}}
|
||||
healths["anchor"] = inventory.NodeHealth{Node: "anchor"}
|
||||
if _, held := holdingOf(open, healths, bound).heldUnder("laptop", "shop",
|
||||
[]inventory.ResourceHealth{failingConsumer("shop")}); !held {
|
||||
t.Fatal("a consumer is not held under its provider's open needs-operator condition")
|
||||
}
|
||||
// A provider healthy holds nothing: what the consumer finds is its own.
|
||||
healthy := waitingDatabase()
|
||||
healthy.State, healthy.Waits = link.StateHealthy, nil
|
||||
healths["anchor"] = inventory.NodeHealth{Node: "anchor", Resources: []inventory.ResourceHealth{healthy}}
|
||||
if _, held := holdingOf(nil, healths, bound).heldUnder("laptop", "shop",
|
||||
[]inventory.ResourceHealth{failingConsumer("shop")}); held {
|
||||
t.Fatal("a consumer of a healthy provider is held")
|
||||
}
|
||||
}
|
||||
|
||||
// (1) The consumer raises nothing of its own, and the provider's needs-operator condition says who waits on it.
|
||||
func TestAWaitingProvidersConditionListsWhoWaitsOnIt(t *testing.T) {
|
||||
k, _ := withConditionsInMemory(t)
|
||||
ctx := t.Context()
|
||||
healths := map[string]inventory.NodeHealth{
|
||||
"anchor": {Node: "anchor", Resources: []inventory.ResourceHealth{waitingDatabase()}},
|
||||
"laptop": {Node: "laptop", Resources: []inventory.ResourceHealth{failingConsumer("shop")}},
|
||||
}
|
||||
bound := map[[3]string]catalogue.Chosen{{"laptop", "shop", "postgres-database"}: theDatabase}
|
||||
was := readHoldingFor
|
||||
t.Cleanup(func() { readHoldingFor = was })
|
||||
readHoldingFor = func(_ context.Context, _ *inventory.Inventory, open []conditions.Condition) (*holding, error) {
|
||||
return holdingOf(open, healths, bound), nil
|
||||
}
|
||||
for look := 1; look <= 2; look++ {
|
||||
if err := judgeModuleHealth(ctx, nil, k, "anchor", map[string][]inventory.ResourceHealth{"db": {waitingDatabase()}},
|
||||
map[string]int{"db": look}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := judgeModuleHealth(ctx, nil, k, "laptop", map[string][]inventory.ResourceHealth{"shop": {failingConsumer("shop")}},
|
||||
map[string]int{"shop": look}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
open, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var keys []string
|
||||
var provider *conditions.Condition
|
||||
for i, c := range open {
|
||||
keys = append(keys, c.Key)
|
||||
if c.Key == needsOperatorKey("db", "anchor") {
|
||||
provider = &open[i]
|
||||
}
|
||||
}
|
||||
if len(open) != 1 || provider == nil {
|
||||
t.Fatalf("a provider waiting for the operator and a consumer failing on it raised %v; want the provider's "+
|
||||
"needs-operator alone", keys)
|
||||
}
|
||||
if said := provider.Evidence[0].Said; !strings.Contains(said, "shop on laptop") {
|
||||
t.Fatalf("the provider's needs-operator does not list shop on laptop as waiting on it: %s", said)
|
||||
}
|
||||
// Its words still name the act: it is the operator's, whoever waits on it.
|
||||
if provider.Resolver != conditions.ResolverOperator || !strings.Contains(provider.Needs, "desk prompt") {
|
||||
t.Fatalf("the provider's condition: %+v", provider)
|
||||
}
|
||||
}
|
||||
|
||||
// relogin and userUnit are person_wait_test.go's; mounts is said here with the same account and unit beside its
|
||||
// wait for the password.
|
||||
func reloginBesideAWait() []inventory.ResourceHealth {
|
||||
return []inventory.ResourceHealth{relogin("mounts", "operator"), userUnit("mounts", "operator"),
|
||||
waitingResource(passwordWait)}
|
||||
}
|
||||
|
||||
// (2) A module with a checked wait beside a relogin-needed account says both: the new login, and the act the
|
||||
// operator owes it.
|
||||
func TestAWaitBesideAReloginIsSaid(t *testing.T) {
|
||||
k, _ := withConditionsInMemory(t)
|
||||
ctx := t.Context()
|
||||
for look := 1; look <= 2; look++ {
|
||||
if err := judgeModuleHealth(ctx, nil, k, "workstation", map[string][]inventory.ResourceHealth{"mounts": reloginBesideAWait()},
|
||||
map[string]int{"mounts": look}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, open := needsOperatorOpen(t, k)
|
||||
if got == nil {
|
||||
t.Fatalf("a wait for the operator beside a relogin is not said: %v", openKeysOf(open))
|
||||
}
|
||||
if !strings.Contains(got.Summary, "smb-password-games") || got.Severity != conditions.Warning {
|
||||
t.Fatalf("the needs-operator beside the relogin: %+v", got)
|
||||
}
|
||||
relogged := false
|
||||
for _, c := range open {
|
||||
relogged = relogged || c.Key == reloginKey("mounts", "workstation")
|
||||
}
|
||||
if !relogged {
|
||||
t.Fatalf("the relogin is no longer said beside the wait: %v", openKeysOf(open))
|
||||
}
|
||||
// The login done, the wait stays said and the relogin clears.
|
||||
if err := judgeModuleHealth(ctx, nil, k, "workstation", map[string][]inventory.ResourceHealth{"mounts": {waitingResource(passwordWait)}},
|
||||
map[string]int{"mounts": 3}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, open = needsOperatorOpen(t, k)
|
||||
if got == nil || len(open) != 1 {
|
||||
t.Fatalf("after the new login: %v", openKeysOf(open))
|
||||
}
|
||||
}
|
||||
|
||||
// (2) The same beside a directory used as found.
|
||||
func TestAWaitBesideADirectoryUsedAsFoundIsSaid(t *testing.T) {
|
||||
k, _ := withConditionsInMemory(t)
|
||||
ctx := t.Context()
|
||||
found := foundDirectory("mounts", time.Now().Add(-time.Hour))
|
||||
for look := 1; look <= 2; look++ {
|
||||
if err := judgeModuleHealth(ctx, nil, k, "workstation", map[string][]inventory.ResourceHealth{
|
||||
"mounts": {found, waitingResource(passwordWait)}}, map[string]int{"mounts": look}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, open := needsOperatorOpen(t, k)
|
||||
if got == nil {
|
||||
t.Fatalf("a wait for the operator beside a directory used as found is not said: %v", openKeysOf(open))
|
||||
}
|
||||
asFound := false
|
||||
for _, c := range open {
|
||||
asFound = asFound || c.Key == usedAsFoundKey("mounts", "workstation")
|
||||
}
|
||||
if !asFound {
|
||||
t.Fatalf("the directory used as found is no longer said beside the wait: %v", openKeysOf(open))
|
||||
}
|
||||
}
|
||||
|
||||
// (2) Beside a fault of its own, the wait is said too, and the fault's words do not count the waiting part among
|
||||
// what fails.
|
||||
func TestAWaitBesideAFaultIsSaidAndTheFaultIsTheFaultAlone(t *testing.T) {
|
||||
k, _ := withConditionsInMemory(t)
|
||||
ctx := t.Context()
|
||||
down := inventory.ResourceHealth{Module: "mounts", Resource: "mounts.apply", Kind: "process",
|
||||
Target: "mesh-mounts-apply.service", State: link.StateUnhealthy, Reason: "down"}
|
||||
for look := 1; look <= 2; look++ {
|
||||
if err := judgeModuleHealth(ctx, nil, k, "workstation", map[string][]inventory.ResourceHealth{
|
||||
"mounts": {down, waitingResource(passwordWait)}}, map[string]int{"mounts": look}, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, open := needsOperatorOpen(t, k)
|
||||
if got == nil {
|
||||
t.Fatalf("a wait for the operator beside a fault is not said: %v", openKeysOf(open))
|
||||
}
|
||||
var fault *conditions.Condition
|
||||
for i, c := range open {
|
||||
if c.Key == moduleUnhealthyKey("mounts", "workstation") {
|
||||
fault = &open[i]
|
||||
}
|
||||
}
|
||||
if fault == nil {
|
||||
t.Fatalf("the fault is not said: %v", openKeysOf(open))
|
||||
}
|
||||
if strings.Contains(fault.Summary, "mounts.watch") {
|
||||
t.Fatalf("the fault's summary counts the waiting part as failing: %q", fault.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func openKeysOf(cs []conditions.Condition) []string {
|
||||
var out []string
|
||||
for _, c := range cs {
|
||||
out = append(out, c.Key)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user