The pipeline was observable from a merge to an artifact and went dark where it touched a machine: a node's report is control traffic only the control plane reads, so nothing said which version a machine runs, or that it refused to (novox/hq ADR 0134). The control plane now states both under the seat it holds — a role's events belong to the role and keep their address when the holder is replaced — and only when the report is news, because a machine reconciles every minute and a fact per report would be a fact per minute per machine. Whether a report is news is the store's answer: it holds the previous one, so the listener returns it and the server states the fact. That also gives the catch-up replay a subject the controller may publish: it was published as a module's event from a module called "control-plane", which does not exist, so the controller's own account refused it and every catalogue that asked what it missed was answered with nothing.
221 lines
7.7 KiB
Go
221 lines
7.7 KiB
Go
package link_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// Turning what a node said into what the mesh keeps.
|
|
//
|
|
// This is where "refused" and "failed" become different things. They are different situations
|
|
// with different remedies — one is fixed in what was sent and the other on the machine — and the
|
|
// mapping deciding which is which had no test at all.
|
|
|
|
func heardFrom(t *testing.T, report link.Report) (*inventory.Inventory, inventory.Doing, bool) {
|
|
t.Helper()
|
|
inv := inventory.ForTest(t)
|
|
ctx := context.Background()
|
|
if _, err := inv.AddNode(ctx, report.Node); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doing, said, err := inv.DoingOf(ctx, report.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return inv, doing, said
|
|
}
|
|
|
|
func TestARefusalIsKeptAsARefusalWithTheNodesOwnWords(t *testing.T) {
|
|
_, doing, said := heardFrom(t, link.Report{
|
|
Node: "workstation", Refused: `resource "conf": a file needs a path`,
|
|
})
|
|
if !said {
|
|
t.Fatal("a refusal was not recorded")
|
|
}
|
|
if doing.Outcome != inventory.OutcomeRefused {
|
|
t.Fatalf("a refusal was recorded as %q", doing.Outcome)
|
|
}
|
|
if !strings.Contains(doing.Refused, "needs a path") {
|
|
// The host says exactly what it could not accept. Anything this end wrote instead would
|
|
// be a second, worse explanation of the same thing.
|
|
t.Fatalf("the node's own words were not kept: %q", doing.Refused)
|
|
}
|
|
}
|
|
|
|
func TestSomeOfItFailingIsNotARefusal(t *testing.T) {
|
|
// Refused means the machine is exactly as it was. Failed means it is in a state nobody
|
|
// declared. Reporting one as the other sends somebody to the wrong place.
|
|
_, doing, _ := heardFrom(t, link.Report{
|
|
Node: "workstation",
|
|
Applied: []string{"a", "b", "c"},
|
|
Failed: map[string]string{"svc": "unit not found", "pkg": "no such package"},
|
|
})
|
|
if doing.Outcome != inventory.OutcomeFailed {
|
|
t.Fatalf("a partial failure was recorded as %q", doing.Outcome)
|
|
}
|
|
if doing.Applied != 3 {
|
|
t.Fatalf("what did apply was not kept: %d", doing.Applied)
|
|
}
|
|
if len(doing.Failed) != 2 {
|
|
t.Fatalf("got %+v", doing.Failed)
|
|
}
|
|
// Ordered, so two readings of one failure are the same reading.
|
|
if doing.Failed[0].ID != "pkg" || doing.Failed[1].ID != "svc" {
|
|
t.Fatalf("failures came back unordered: %+v", doing.Failed)
|
|
}
|
|
}
|
|
|
|
func TestACleanApplyIsRecordedAsOne(t *testing.T) {
|
|
inv, doing, _ := heardFrom(t, link.Report{Node: "workstation", Applied: []string{"a", "b"}})
|
|
if doing.Outcome != inventory.OutcomeApplied || doing.Applied != 2 {
|
|
t.Fatalf("got %+v", doing)
|
|
}
|
|
wrong, err := inv.NotDoingWhatTheyWereTold(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(wrong) != 0 {
|
|
t.Fatalf("a clean apply is listed as wrong: %+v", wrong)
|
|
}
|
|
}
|
|
|
|
func TestABareAliveDoesNotWipeTheDeclarationThatSaysANodeIsCurrent(t *testing.T) {
|
|
// A node reports it is alive every minute and describes what it applied rarely. If a bare
|
|
// alive were written down as a report it would replace the last real apply with an empty one
|
|
// — clearing the declaration digest current is measured against — so a node that had just
|
|
// caught up would read as behind within the minute, and never converge. The lab saw exactly
|
|
// this: a heavy wave whose apply outran the first heartbeat never reached `current`.
|
|
inv := inventory.ForTest(t)
|
|
ctx := context.Background()
|
|
node, err := inv.AddNode(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The mesh sent this node a declaration, and the node applied it and named which by digest.
|
|
const digest = "d640d1b6a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071829304152"
|
|
if err := inv.RecordSent(ctx, node.ID, digest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{
|
|
Node: "anchor", Applied: []string{"a", "b"}, Declared: digest, Carried: []int{5432},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
currentOf := func(name string) bool {
|
|
reports, err := inv.LastReports(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range reports {
|
|
if r.Node == name {
|
|
return r.Current
|
|
}
|
|
}
|
|
t.Fatalf("no report for %s", name)
|
|
return false
|
|
}
|
|
if !currentOf("anchor") {
|
|
t.Fatal("a node that applied exactly what it was sent does not read as current")
|
|
}
|
|
|
|
// Now the node says only that it is there, as it does every minute.
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !currentOf("anchor") {
|
|
t.Fatal("a bare alive wiped the declaration digest, so a current node now reads as behind")
|
|
}
|
|
// And the last real account of what it did and holds is untouched.
|
|
doing, said, err := inv.DoingOf(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !said || doing.Outcome != inventory.OutcomeApplied || doing.Applied != 2 {
|
|
t.Fatalf("a bare alive overwrote the last real report: said=%v %+v", said, doing)
|
|
}
|
|
owned, _, err := inv.Owned(ctx, node.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(owned) != 2 {
|
|
t.Fatalf("a bare alive replaced the account of what the machine holds: %v", owned)
|
|
}
|
|
}
|
|
|
|
func TestAFailureDoesNotBecomeTheAccountOfWhatTheMachineHolds(t *testing.T) {
|
|
// A partial list is not an account of what the machine holds. Recording one as though it
|
|
// were would tell a rebuilding node to remove what it still has — which is the fault that
|
|
// destroyed a foundation once (novox/hq 04-ISSUES/010).
|
|
inv := inventory.ForTest(t)
|
|
ctx := context.Background()
|
|
node, err := inv.AddNode(ctx, "workstation")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordOwned(ctx, node.ID, []string{"one", "two", "three"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{
|
|
Node: "workstation", Applied: []string{"one"}, Failed: map[string]string{"two": "no"},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owned, _, err := inv.Owned(ctx, node.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(owned) != 3 {
|
|
t.Fatalf("a partial report replaced the account of what the machine holds: %v", owned)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0100: what an adopted node holds, the firewall it found and what is reachable on it
|
|
// are kept from the report that carries them, and a bare word that the node is there wipes none.
|
|
func TestWhatAnAdoptedNodeHoldsIsKeptAndAnAliveWordDoesNotWipeIt(t *testing.T) {
|
|
inv, _, _ := heardFrom(t, link.Report{
|
|
Node: "anchor", Applied: []string{"hello-web.served"}, Firewall: "ufw",
|
|
Held: []link.Held{{ID: "hello-web.page", Module: "hello-web", Kind: "file",
|
|
Target: "/var/lib/hello-web/index.html", Changed: "rewritten", Kept: "/var/lib/mesh/kept/x"}},
|
|
Reachable: []link.Reach{{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web",
|
|
Published: true, ContainerPort: 80}},
|
|
})
|
|
ctx := context.Background()
|
|
check := func(when string) {
|
|
t.Helper()
|
|
got, err := inv.AdoptionOf(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Firewall != "ufw" || len(got.Held) != 1 || got.Held[0].Changed != "rewritten" ||
|
|
len(got.Reachable) != 1 || got.Reachable[0].ContainerPort != 80 || got.At.IsZero() {
|
|
t.Fatalf("%s: what the node said is not what was kept: %+v", when, got)
|
|
}
|
|
}
|
|
check("after the report")
|
|
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check("after an alive word")
|
|
|
|
// A reconcile report carrying only adoption is recorded, though it applied nothing.
|
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor",
|
|
Firewall: "ufw"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.AdoptionOf(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Held) != 0 || got.Firewall != "ufw" {
|
|
t.Fatalf("a report from an adopted node holding nothing did not empty held: %+v", got)
|
|
}
|
|
}
|