Keep what an adopted node reports holding, the firewall it found and what is reachable on it (hq ADR 0100)

This commit is contained in:
2026-09-22 17:23:09 +02:00
parent c3b1617693
commit 28894fa5bd
7 changed files with 253 additions and 2 deletions
+21
View File
@@ -199,6 +199,27 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (err error) {
return err
}
// What an adopted node holds, which firewall it found, and what is reachable on it (novox/hq
// ADR 0100). Recorded whenever a report carries any of it — a node reports these on its own
// schedule, when what it holds changes, not only after an apply — and never cleared by a
// report that carries none, which is every bare word that the node is there. An adopted node
// always names its firewall, so a report from one replaces all three, emptied held included.
if len(report.Held) > 0 || report.Firewall != "" || len(report.Reachable) > 0 {
held := make([]inventory.Held, 0, len(report.Held))
for _, h := range report.Held {
held = append(held, inventory.Held{ID: h.ID, Module: h.Module, Kind: h.Kind,
Target: h.Target, Since: h.Since, Changed: h.Changed, Kept: h.Kept})
}
reachable := make([]inventory.Reach, 0, len(report.Reachable))
for _, r := range report.Reachable {
reachable = append(reachable, inventory.Reach{Protocol: r.Protocol, Address: r.Address,
Port: r.Port, By: r.By, Published: r.Published, ContainerPort: r.ContainerPort})
}
if err := e.Inventory.RecordAdoption(ctx, node.ID, held, report.Firewall, reachable); err != nil {
return err
}
}
// A bare word that a node is there is not an account of what the machine did or holds: it
// moves last_seen and touches nothing else. This arrives every minute (link.AliveEvery),
// while a real report is rare, so recording it as one would overwrite the node's last real
+43
View File
@@ -175,3 +175,46 @@ func TestAFailureDoesNotBecomeTheAccountOfWhatTheMachineHolds(t *testing.T) {
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)
}
}
+41 -1
View File
@@ -6,7 +6,10 @@
// traffic between them, each receiving half of what it expects. That has happened here before.
package link
import "encoding/base64"
import (
"encoding/base64"
"time"
)
// Exchange is where nodes publish everything they have to say.
const Exchange = "mesh"
@@ -116,6 +119,43 @@ type Report struct {
// Declared is the digest of the declaration this report is about — the same bytes, hashed
// the same way, as the `sent` digest the mesh recorded. Which declaration, not when.
Declared string `json:"declared,omitempty"`
// Held is what an adopted node found and is keeping as it was until its module is taken
// (novox/hq ADR 0100). Without it an adopted node reads as converged.
Held []Held `json:"held,omitempty"`
// Firewall is the firewall found on the machine — "ufw" or "none" — and empty on a node that
// was never asked, which is every converged one.
Firewall string `json:"firewall,omitempty"`
// Reachable is what can be reached on the machine now: every listening socket and every
// published container port. Only an adopted node reports it; it is what converging previews.
Reachable []Reach `json:"reachable,omitempty"`
}
// Held is one file or container found on an adopted node and kept as it was.
type Held struct {
ID string `json:"id"`
Module string `json:"module"`
Kind string `json:"kind"`
Target string `json:"target"`
Since time.Time `json:"since"`
// Changed is what something other than the mesh did to it since — rewritten, stopped,
// replaced or gone — and empty while it is as found.
Changed string `json:"changed,omitempty"`
// Kept is where a file's original was kept.
Kept string `json:"kept,omitempty"`
}
// Reach is one thing reachable on the machine: a listening socket, or a published container port.
type Reach struct {
Protocol string `json:"protocol"`
Address string `json:"address"`
Port int `json:"port"`
// By is what holds it — a process, or a container's name.
By string `json:"by,omitempty"`
// Published is a container port the runtime publishes, reached on the forwarded path; its
// container's own port is ContainerPort.
Published bool `json:"published,omitempty"`
ContainerPort int `json:"container-port,omitempty"`
}
// EnrolReply is what the mesh says back.
+7
View File
@@ -16,6 +16,13 @@ func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
{Signed{Declaration: []byte("{}"), Signature: []byte("x")}, []string{"declaration", "signature"}},
{Report{Node: "n", Applied: []string{"a"}, Failed: map[string]string{"k": "v"}, Refused: "r"},
[]string{"node", "applied", "failed", "refused"}},
{Report{Node: "n", Held: []Held{{ID: "m.f"}}, Firewall: "ufw", Reachable: []Reach{{Port: 1}}},
[]string{"node", "held", "firewall", "reachable"}},
{Held{ID: "m.f", Module: "m", Kind: "file", Target: "/f", Changed: "rewritten", Kept: "/k"},
[]string{"id", "module", "kind", "target", "since", "changed", "kept"}},
{Reach{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web", Published: true,
ContainerPort: 80},
[]string{"protocol", "address", "port", "by", "published", "container-port"}},
{EnrolRequest{Node: "n", Secret: "s", PublicKey: []byte("k")},
[]string{"node", "secret", "public_key"}},
} {