Files
mesh-controller/cmd/mesh-controller/readable_test.go
T
jschoubben b5df244096 The mesh says what filters a converged machine: filters kept per node, shown by node show, named by status, and previewed with their fates (hq ADR 0168)
A host reports every table and chain that refuses traffic with its owner,
and a converged machine's found firewall's state. The controller keeps both
on the node's record (migration 0054), shows them on node show, names every
converged machine something other than the mesh filters in status — text
and JSON, and such a machine is not well — and the converge preview lists
what filters the machine with the fate of each: retired with the front end,
left as the runtime's, left as a ban, or left in force and not the mesh's.
What was invisible for eleven hours (issues 144, 145) is said by name.
2026-10-02 12:00:12 +02:00

213 lines
8.3 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/inventory"
)
// The shape something other than a person reads.
//
// Hard to change once anything is built against it, so it stays close to what the domain already
// calls things and carries no summary field that would have to be kept true.
func statusOf(t *testing.T, wrong []inventory.Doing, nodes, quiet []inventory.Node,
behind map[string][]string, sources map[string]inventory.Source) map[string]any {
t.Helper()
body, err := statusAsJSON(answers{
wrong: wrong, nodes: nodes, quiet: quiet, behind: behind, sources: sources})
if err != nil {
t.Fatal(err)
}
var parsed map[string]any
if err := json.Unmarshal(body, &parsed); err != nil {
t.Fatalf("what a board would read is not JSON: %v", err)
}
return parsed
}
func TestRefusedAndFailedStayDistinctAllTheWayOut(t *testing.T) {
// They are fixed in different places, so one word for both would send half the readers of a
// page to the wrong one.
got := statusOf(t,
[]inventory.Doing{
{Node: "one", Outcome: inventory.OutcomeRefused, Refused: "a file needs a path"},
{Node: "two", Outcome: inventory.OutcomeFailed, Applied: 3,
Failed: []inventory.FailedResource{{ID: "shell.pkg", Error: "target not found"}}},
},
[]inventory.Node{{Name: "one"}, {Name: "two"}}, nil, nil, nil)
wrong, _ := got["wrong"].([]any)
if len(wrong) != 2 {
t.Fatalf("got %v", got["wrong"])
}
first, _ := wrong[0].(map[string]any)
if first["outcome"] != inventory.OutcomeRefused || first["refused"] == nil {
t.Fatalf("a refusal did not survive: %v", first)
}
second, _ := wrong[1].(map[string]any)
if second["outcome"] != inventory.OutcomeFailed {
t.Fatalf("a failure did not survive: %v", second)
}
if second["applied"] != float64(3) {
// "Three of eight" and "none of eight" are different machines.
t.Fatalf("what did apply was not carried: %v", second)
}
}
func TestAMachineThatNeverSpokeSaysSoByOmission(t *testing.T) {
// Never heard from and quiet for a while are different situations, and a zero time would read
// as a date in 1970 on any page that formatted it.
got := statusOf(t, nil,
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
nil, nil)
quiet, _ := got["quiet"].([]any)
if len(quiet) != 2 {
t.Fatalf("got %v", got["quiet"])
}
never, _ := quiet[0].(map[string]any)
if _, said := never["lastSeen"]; said {
t.Fatalf("a machine that never spoke carries a time: %v", never)
}
away, _ := quiet[1].(map[string]any)
if _, said := away["lastSeen"]; !said {
t.Fatalf("a machine that has been quiet carries no time: %v", away)
}
}
func TestNothingWrongIsAnEmptyListRatherThanNothing(t *testing.T) {
// A page distinguishing "no machines are wrong" from "this field is missing" would have to
// handle both, and null is the one that gets forgotten.
got := statusOf(t, nil, []inventory.Node{{Name: "a", LastSeen: time.Now()}}, nil, nil, nil)
for _, key := range []string{"wrong", "quiet", "behind"} {
list, ok := got[key].([]any)
if !ok {
t.Fatalf("%q is %T, not a list", key, got[key])
}
if len(list) != 0 {
t.Fatalf("%q is not empty: %v", key, list)
}
}
// And how many machines there are, so a reader can tell "none wrong" from "none at all".
if got["machines"] != float64(1) {
t.Fatalf("got %v", got["machines"])
}
}
func TestWhatIsBehindNamesTheMachinesRunningTheOldOne(t *testing.T) {
// A module being out of date is a fact about the catalogue; machines running the old one is
// the thing with consequences.
got := statusOf(t, nil, nil, nil,
map[string][]string{"shell": {"workstation", "laptop"}},
map[string]inventory.Source{"shell": {BuiltFrom: "aaaaaaa1", Head: "bbbbbbb2"}})
behind, _ := got["behind"].([]any)
if len(behind) != 1 {
t.Fatalf("got %v", got["behind"])
}
row, _ := behind[0].(map[string]any)
if row["module"] != "shell" || row["builtFrom"] != "aaaaaaa1" || row["head"] != "bbbbbbb2" {
t.Fatalf("got %v", row)
}
on, _ := row["on"].([]any)
if len(on) != 2 {
t.Fatalf("the machines running the old one are not named: %v", row)
}
}
func TestNoSecretIsInWhatABoardReads(t *testing.T) {
// Everything here comes from the mesh's own records, which hold no readable secret — but a
// shape a page is built against is exactly where one would eventually be added for
// convenience, so this says it out loud.
body, err := statusAsJSON(answers{
wrong: []inventory.Doing{{Node: "a", Outcome: inventory.OutcomeRefused,
Refused: "resource \"x\": a file needs a path"}},
nodes: []inventory.Node{{Name: "a"}},
refused: map[string]string{"a": "nothing provides \"database\", wanted by web"},
})
if err != nil {
t.Fatal(err)
}
for _, word := range []string{"password", "secret", "sealed", "credential", "token"} {
if strings.Contains(strings.ToLower(string(body)), word) {
t.Fatalf("what a board reads carries a %q field:\n%s", word, body)
}
}
}
// A failure that has been reported identically enough times is said to be stuck, with when it
// began and how many times — so a board can tell a machine looping on something that will never
// apply from one that failed a minute ago (novox/hq 04-ISSUES/065, ADR 0090).
func TestAMachineFailingTheSameWayIsSaidToBeStuck(t *testing.T) {
began := time.Date(2026, 9, 21, 9, 0, 0, 0, time.UTC)
got := statusOf(t,
[]inventory.Doing{
{Node: "looping", Outcome: inventory.OutcomeFailed, Since: &began, Times: inventory.StuckAfter,
Failed: []inventory.FailedResource{{ID: "img", Error: "no such image"}}},
{Node: "once", Outcome: inventory.OutcomeFailed, Since: &began, Times: 1,
Failed: []inventory.FailedResource{{ID: "img", Error: "no such image"}}},
},
[]inventory.Node{{Name: "looping"}, {Name: "once"}}, nil, nil, nil)
wrong, _ := got["wrong"].([]any)
looping, _ := wrong[0].(map[string]any)
if looping["stuck"] != true || looping["times"] != float64(inventory.StuckAfter) {
t.Fatalf("three identical failures are stuck: %v", looping)
}
if since, _ := looping["since"].(string); !strings.HasPrefix(since, "2026-09-21T09:00:00") {
t.Fatalf("when it began was not carried: %v", looping)
}
once, _ := wrong[1].(map[string]any)
if once["stuck"] != false || once["times"] != float64(1) {
t.Fatalf("one failure is not stuck: %v", once)
}
}
// A converged machine something other than the mesh filters is named, per rule set, and is not
// well (novox/hq ADR 0168); one filtered by the mesh alone is not in the list.
func TestAMachineNotFilteredByTheMeshAloneIsNamedAndNotWell(t *testing.T) {
alone := inventory.Filtering{Filters: []inventory.Filter{
{Where: "table inet mesh, chain input", Owner: inventory.FilterMesh, Refuses: "policy drop"},
{Where: "table ip filter, chain DOCKER", Owner: inventory.FilterRuntime, Refuses: "drop"},
{Where: "table ip filter, chain f2b-sshd", Owner: inventory.FilterBan, Refuses: "ip saddr 192.0.2.1 reject"},
}}
if !alone.Alone() {
t.Fatal("the mesh's own, the runtime's and a ban are not the mesh alone")
}
notAlone := inventory.Filtering{
Filters: append(alone.Filters, inventory.Filter{Where: "chain HAL-MESH-ONLY (iptables-legacy)",
Owner: inventory.FilterOther, Refuses: `-A HAL-MESH-ONLY -m comment --comment "not public" -j DROP`}),
FoundFirewall: &inventory.FoundFirewall{Kind: "ufw", Active: true},
}
asked := answers{nodes: []inventory.Node{{Name: "home-server"}, {Name: "laptop"}},
filtered: map[string]inventory.Filtering{"home-server": notAlone}}
if asked.well() {
t.Fatal("a machine not filtered by the mesh alone reads as well")
}
body, err := statusAsJSON(asked)
if err != nil {
t.Fatal(err)
}
var parsed struct {
Filtered []map[string]string `json:"filtered"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
t.Fatal(err)
}
if len(parsed.Filtered) != 2 {
t.Fatalf("filtered: %v", parsed.Filtered)
}
if parsed.Filtered[0]["node"] != "home-server" || parsed.Filtered[0]["owner"] != inventory.FilterFoundFirewall ||
parsed.Filtered[1]["where"] != "chain HAL-MESH-ONLY (iptables-legacy)" || parsed.Filtered[1]["owner"] != inventory.FilterOther {
t.Fatalf("filtered: %v", parsed.Filtered)
}
if body, _ := statusAsJSON(answers{nodes: asked.nodes}); strings.Contains(string(body), `"filtered"`) {
t.Fatal("a mesh filtered by itself alone carries a filtered list")
}
}