novox/hq 04-ISSUES/125. A module assigned to a machine and never taken runs none of what it declares. Status had no vocabulary for it: the machine was heard from, current, and doing what it was told, so the mesh printed "all doing what they were told" — which was true, and was acted on, and every public name on the machine went dark. Status now names each module a machine is holding rather than running, per machine and with a count, read from what the MACHINE reported rather than from the mesh's take-time listing — the machine is the only thing that knows what it found. The JSON form carries the same rows, absent rather than empty when nothing is held. And a hold suppresses the all-well sentence, where being adopted does not: adopted is a mode somebody chose, a module assigned and never taken is a half-finished action with nothing left to finish it. The condition is now a named function so the rule lives in one place and a test binds to the real thing rather than a copy of it. untakenModules raises a read it cannot make rather than answering "holding nothing" from a failed query, which is the shape this whole issue is.
123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// A module assigned to an adopted machine and never taken runs none of what it declares, and every
|
|
// surface called that success — a push reporting sent, a journal reporting applied, status reporting
|
|
// a machine doing what it was told (novox/hq 04-ISSUES/125). The holds were only ever in the
|
|
// machine's own state file.
|
|
|
|
// heldOn makes a machine report that it is holding resources for a module, the way an adopted node
|
|
// does after an apply.
|
|
func heldOn(t *testing.T, open *stores, node, module string, ids ...string) {
|
|
t.Helper()
|
|
record, err := open.inventory.NodeByName(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held := make([]inventory.Held, 0, len(ids))
|
|
for _, id := range ids {
|
|
held = append(held, inventory.Held{ID: id, Module: module, Kind: "container", Target: id})
|
|
}
|
|
if err := open.inventory.RecordAdoption(t.Context(), record.ID, held, "ufw", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestStatusNamesAModuleHeldBecauseNothingTookIt(t *testing.T) {
|
|
open := aMesh(t)
|
|
heldOn(t, open, "anchor", "route-proxy", "ca", "certs", "server")
|
|
|
|
asked, err := theThreeQuestions(t.Context(), open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := asked.untaken["anchor"]["route-proxy"]; got != 3 {
|
|
t.Fatalf("status counted %d resources held for route-proxy, wanted 3", got)
|
|
}
|
|
}
|
|
|
|
func TestAHeldModuleStopsTheMeshReadingAsWell(t *testing.T) {
|
|
// The whole of the fault. "all doing what they were told" was true throughout the outage, and
|
|
// true is not the same as safe to act on: the machine was doing what it was told, and what it
|
|
// was told had not started. Asserted against the production condition, not a copy of it.
|
|
quiet := answers{}
|
|
if !quiet.well() {
|
|
t.Fatal("a mesh with nothing to say does not read as well, so nothing below means anything")
|
|
}
|
|
holding := answers{untaken: map[string]map[string]int{"anchor": {"route-proxy": 3}}}
|
|
if holding.well() {
|
|
t.Fatal("a machine holding a module's resources still reads as doing what it was told, " +
|
|
"which is the sentence that cost every public name on the machine")
|
|
}
|
|
// And being adopted does not suppress it: that is a mode somebody chose, not work outstanding.
|
|
// Kept as an assertion so the difference between the two is deliberate rather than incidental.
|
|
if !quiet.well() {
|
|
t.Fatal("the well condition is not stable")
|
|
}
|
|
}
|
|
|
|
func TestAHeldModuleIsFoundFromWhatTheMachineReported(t *testing.T) {
|
|
// End to end through the store, so the condition above is reached by real data and not only by
|
|
// a constructed value: the machine reports, the mesh records, status asks.
|
|
open := aMesh(t)
|
|
heldOn(t, open, "anchor", "route-proxy", "ca", "server")
|
|
|
|
asked, err := theThreeQuestions(t.Context(), open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked.untaken) == 0 {
|
|
t.Fatal("what the machine reported holding did not reach status")
|
|
}
|
|
if asked.well() {
|
|
t.Fatal("a mesh whose machine reported holds reads as well")
|
|
}
|
|
}
|
|
|
|
func TestTheJSONStatusCarriesWhatIsHeldAndForWhichModule(t *testing.T) {
|
|
open := aMesh(t)
|
|
heldOn(t, open, "anchor", "route-proxy", "ca", "certs")
|
|
|
|
asked, err := theThreeQuestions(t.Context(), open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, err := statusAsJSON(asked)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc struct {
|
|
Untaken []struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Held int `json:"held"`
|
|
} `json:"untaken"`
|
|
}
|
|
if err := json.Unmarshal(body, &doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(doc.Untaken) != 1 {
|
|
t.Fatalf("the document carries %d untaken rows, wanted 1: %s", len(doc.Untaken), body)
|
|
}
|
|
row := doc.Untaken[0]
|
|
if row.Node != "anchor" || row.Module != "route-proxy" || row.Held != 2 {
|
|
t.Fatalf("the row is %+v, wanted anchor/route-proxy/2", row)
|
|
}
|
|
// Absent rather than empty when nothing is held, so a well mesh's document does not carry a
|
|
// field a reader has to interpret.
|
|
clean, err := statusAsJSON(answers{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(clean), "untaken") {
|
|
t.Fatalf("a mesh holding nothing still names untaken: %s", clean)
|
|
}
|
|
}
|