mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/a-module-says-how-it-is-healthy delivered: every member is delivered
A container that crash-looped after its compose applied passed every check the gate had: nothing looked at what a module runs. The node-engine now judges every long-running resource on every look — one read of the runtime, one per service manager — keeps the restarts it counts across recreates and its own restarts, and says the state in every report and as an event on change, again every minute while not healthy. It reads only; nothing is restarted for being unhealthy.
163 lines
6.2 KiB
Go
163 lines
6.2 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// verified runs what Run does to a delivery body, without a broker: unmarshal, check the
|
|
// signature, and only then apply. Isolating it keeps this test about the check rather than about
|
|
// the bus, which is tested against a real one in the lab.
|
|
func verified(t *testing.T, signer ed25519.PublicKey, body []byte) (Report, bool) {
|
|
t.Helper()
|
|
applied := false
|
|
report := handleBody(context.Background(), Membership{Node: "anchor", Signer: signer}, body,
|
|
func(context.Context, []byte, []byte) Report {
|
|
applied = true
|
|
return Report{Applied: []string{"something"}}
|
|
})
|
|
return report, applied
|
|
}
|
|
|
|
func signedBody(t *testing.T, private ed25519.PrivateKey, declaration string) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(Signed{
|
|
Declaration: []byte(declaration),
|
|
Signature: ed25519.Sign(private, []byte(declaration)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func TestTheMeshsOwnDeclarationIsApplied(t *testing.T) {
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, applied := verified(t, public, signedBody(t, private, `{"declaration":1}`))
|
|
if !applied {
|
|
t.Fatalf("a declaration the mesh signed was not applied: %s", report.Refused)
|
|
}
|
|
}
|
|
|
|
// Defends novox/hq ADR 0002: everything reaching a node arrives over the broker — and therefore
|
|
// ADR 0004's consequence, that the broker is not trusted to say who is speaking.
|
|
//
|
|
// A transport nobody authenticates per-message would let whatever holds the connection attribute
|
|
// a declaration to any node it liked.
|
|
func TestAForgedDeclarationIsNeverApplied(t *testing.T) {
|
|
// The check that stands between "the mesh changes this machine" and "anybody does". The host
|
|
// applies whatever the link delivers, so a forged declaration is the whole machine.
|
|
public, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, other, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
report, applied := verified(t, public, signedBody(t, other, `{"declaration":1}`))
|
|
if applied {
|
|
t.Fatal("a declaration signed by another key was applied")
|
|
}
|
|
if report.Refused != ErrForged.Error() {
|
|
t.Errorf("refused, but not as a forgery: %q", report.Refused)
|
|
}
|
|
}
|
|
|
|
func TestATamperedDeclarationIsNeverApplied(t *testing.T) {
|
|
// A broker that changed the declaration in flight, keeping the signature. This is what makes
|
|
// pinning the transport insufficient on its own.
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := json.Marshal(Signed{
|
|
Declaration: []byte(`{"declaration":1,"resources":["something else entirely"]}`),
|
|
Signature: ed25519.Sign(private, []byte(`{"declaration":1}`)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
report, applied := verified(t, public, raw)
|
|
if applied {
|
|
t.Fatal("a declaration altered after signing was applied")
|
|
}
|
|
if report.Refused != ErrForged.Error() {
|
|
t.Errorf("refused, but not as a forgery: %q", report.Refused)
|
|
}
|
|
}
|
|
|
|
func TestAMalformedMessageIsToldApartFromAForgery(t *testing.T) {
|
|
// novox/hq ADR 0004 requires these to be distinguishable: one means somebody is trying, the
|
|
// other means something is broken, and they need different responses from a person.
|
|
public, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, applied := verified(t, public, []byte("this is not a message"))
|
|
if applied {
|
|
t.Fatal("something unparseable was applied")
|
|
}
|
|
if report.Refused == ErrForged.Error() {
|
|
t.Error("a malformed message was reported as a forgery; those must be distinguishable")
|
|
}
|
|
}
|
|
|
|
func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
|
|
// The contract with the control plane, which defines these separately. A matching test lives
|
|
// there; rename a field on either side and both fail.
|
|
for _, c := range []struct {
|
|
value any
|
|
expect []string
|
|
}{
|
|
{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"}},
|
|
// novox/hq ADR 0100: what an adopted node holds, the firewall it was found with, and what
|
|
// is reachable on it.
|
|
{Report{Node: "n", Held: []Held{{ID: "i"}}, Firewall: "ufw", Reachable: []Reach{{Port: 1}}},
|
|
[]string{"node", "held", "firewall", "reachable"}},
|
|
{Held{ID: "i", Module: "m", Kind: "file", Target: "/t", Changed: "rewritten", Kept: "/k"},
|
|
[]string{"id", "module", "kind", "target", "since", "changed", "kept"}},
|
|
{Reach{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "c", Published: true, ContainerPort: 80},
|
|
[]string{"protocol", "address", "port", "by", "published", "container-port"}},
|
|
// novox/hq to-be 45 §8: a witness's verdicts, said on every report while they stand, and the
|
|
// witness contract this host keeps.
|
|
{Report{Node: "n", Rollbacks: []Rollback{{Component: ComponentController}}, Witness: WitnessContract},
|
|
[]string{"node", "rollbacks", "witness"}},
|
|
{Rollback{Component: ComponentNodeTools, From: "sha256:b", To: "sha256:a", Outcome: RolledBack, Why: "w"},
|
|
[]string{"component", "from", "to", "outcome", "why", "at"}},
|
|
// novox/hq ADR 0240: every long-running resource's health, in every report and in its own event.
|
|
{Report{Node: "n", Health: &Health{Contract: LivenessContract}}, []string{"node", "health"}},
|
|
{Health{Contract: LivenessContract, Resources: []ResourceHealth{}}, []string{"contract", "at", "resources"}},
|
|
{ResourceHealth{Module: "m", Resource: "m.r", Kind: "container", Target: "t", State: StateUnhealthy,
|
|
Reason: ReasonRestarting, Streak: 2, Restarts: 3},
|
|
[]string{"module", "resource", "kind", "target", "state", "reason", "since", "streak", "restarts"}},
|
|
{HealthSaid{Node: "n"}, []string{"node", "health"}},
|
|
} {
|
|
raw, err := json.Marshal(c.value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fields map[string]any
|
|
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range c.expect {
|
|
if _, ok := fields[want]; !ok {
|
|
t.Errorf("%T has no %q field; the control plane uses that name", c.value, want)
|
|
}
|
|
}
|
|
if len(fields) != len(c.expect) {
|
|
t.Errorf("%T has %d fields, expected %d: %v", c.value, len(fields), len(c.expect), fields)
|
|
}
|
|
}
|
|
}
|