The vault's seat, and a report that carries the machine's profile (hq ADR 0161)
mesh-vault joins the mesh's own set — mesh-scoped, delivering secret — because the controller seals every minted credential with it, which is the test for a seat of the mesh's own; a second provider is a second claimant, refused by name (issue 106). A report may carry the machine's profile, detected again by the apply that reports, and the latest replaces the enrolled one: a machine that switched its network manager is a machine whose uplink holder lacks a capability at its next push (issue 138).
This commit is contained in:
@@ -75,6 +75,10 @@ var defaultSeats = []Seat{
|
|||||||
// rather than receives ambiently — 23 of the catalogue's modules never speak, and an ambient
|
// rather than receives ambiently — 23 of the catalogue's modules never speak, and an ambient
|
||||||
// connection would mint a credential for each.
|
// connection would mint a credential for each.
|
||||||
{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "novox/hq ADR 0079"},
|
{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "novox/hq ADR 0079"},
|
||||||
|
// The vault: the controller seals every minted credential with what it provides, which is the
|
||||||
|
// test for a seat of the mesh's own (novox/hq ADR 0161) — a second provider of `secret` is a
|
||||||
|
// second claimant, refused by name, rather than a candidate for a pin.
|
||||||
|
{Name: "mesh-vault", Scope: ScopeMesh, Delivers: "secret", Decision: "novox/hq ADR 0161"},
|
||||||
// Named for its scope since 2026-09-30 (novox/hq ADR 0156); `the-artifact-store` resolves to it as
|
// Named for its scope since 2026-09-30 (novox/hq ADR 0156); `the-artifact-store` resolves to it as
|
||||||
// an alias on a mesh that predates the rename. It serves artifacts of every kind a build makes —
|
// an alias on a mesh that predates the rename. It serves artifacts of every kind a build makes —
|
||||||
// images and archives, by digest — which is why the provision is the artifact store and not an
|
// images and archives, by digest — which is why the provision is the artifact store and not an
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|||||||
delivered[s.Delivers] = s.Name
|
delivered[s.Delivers] = s.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(Seats()) != 14 {
|
if len(Seats()) != 15 {
|
||||||
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
||||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// The vault's provision is one the controller itself dereferences — every minted credential is
|
||||||
|
// sealed with it — so it is delivered by a seat of the mesh's own, and a second provider is a second
|
||||||
|
// claimant refused by name rather than a candidate for a pin (novox/hq ADR 0161, issue 106).
|
||||||
|
func TestTheVaultsSeatDeliversSecret(t *testing.T) {
|
||||||
|
seat, known := SeatNamed("mesh-vault")
|
||||||
|
if !known {
|
||||||
|
t.Fatal("mesh-vault is not in the mesh's own set")
|
||||||
|
}
|
||||||
|
if seat.Scope != ScopeMesh || seat.Delivers != "secret" {
|
||||||
|
t.Fatalf("mesh-vault is %s-scoped and delivers %q; one per mesh, delivering secret", seat.Scope, seat.Delivers)
|
||||||
|
}
|
||||||
|
vault := Manifest{Module: "mesh-vault", Provides: []Offer{{Name: "secret", Scope: ScopeMesh}},
|
||||||
|
Claims: []Claim{{Name: "mesh-vault", Scope: ScopeMesh}}}
|
||||||
|
if err := CanHold(vault, seat); err != nil {
|
||||||
|
t.Fatalf("the vault, claiming its seat and providing secret, was refused: %v", err)
|
||||||
|
}
|
||||||
|
another := Manifest{Module: "other-vault", Provides: []Offer{{Name: "secret", Scope: ScopeMesh}}}
|
||||||
|
if err := CanHold(another, seat); err == nil {
|
||||||
|
t.Fatal("a provider of secret that does not claim the seat was allowed to hold it")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -320,6 +320,13 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(report.Profile) > 0 {
|
||||||
|
// The latest wins, as at enrolment: a capability the machine lost is one the plan must
|
||||||
|
// stop counting on (novox/hq ADR 0161).
|
||||||
|
if err := e.Inventory.RecordProfile(ctx, node.ID, report.Profile); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
// What it says about the tunnel it carried (novox/hq ADR 0105), whenever it says it.
|
// What it says about the tunnel it carried (novox/hq ADR 0105), whenever it says it.
|
||||||
if report.Tunnel != nil {
|
if report.Tunnel != nil {
|
||||||
if err := e.Inventory.RecordCarriedTunnel(ctx, node.ID, inventory.Carried{
|
if err := e.Inventory.RecordCarriedTunnel(ctx, node.ID, inventory.Carried{
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package link_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A report may carry the machine's profile, detected again by the apply that reports, and the latest
|
||||||
|
// replaces what enrolment recorded (novox/hq ADR 0161): a machine that switched its network manager
|
||||||
|
// is a machine whose uplink holder lacks a capability at its next push, not at its next enrolment.
|
||||||
|
func TestAReportsProfileReplacesTheEnrolledOne(t *testing.T) {
|
||||||
|
e, _, _ := anEnrolledHub(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
first := map[string]any{"capabilities": []any{map[string]any{"name": "uplink-networkmanager", "present": true}}}
|
||||||
|
if _, err := e.Heard(ctx, link.Report{Node: "anchor", Profile: first}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := e.Inventory.Profile(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].Name != "uplink-networkmanager" || !got[0].Present {
|
||||||
|
t.Fatalf("the report's profile was not kept: %+v", got)
|
||||||
|
}
|
||||||
|
// The machine switched managers; the next report says so and the old fact is gone.
|
||||||
|
second := map[string]any{"capabilities": []any{map[string]any{"name": "uplink-systemd-networkd", "present": true}}}
|
||||||
|
if _, err := e.Heard(ctx, link.Report{Node: "anchor", Profile: second}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err = e.Inventory.Profile(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].Name != "uplink-systemd-networkd" {
|
||||||
|
t.Fatalf("the latest profile did not replace the earlier one: %+v", got)
|
||||||
|
}
|
||||||
|
// A report with no profile leaves the last one standing.
|
||||||
|
if _, err := e.Heard(ctx, link.Report{Node: "anchor", Host: "1"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got, _ = e.Inventory.Profile(ctx, "anchor"); len(got) != 1 {
|
||||||
|
t.Fatalf("a report without a profile erased it: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -193,6 +193,12 @@ type Report struct {
|
|||||||
// refuses it whole — which is right, and makes every new field a flag day that the mesh could
|
// refuses it whole — which is right, and makes every new field a flag day that the mesh could
|
||||||
// not see coming.
|
// not see coming.
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
|
|
||||||
|
// Profile is what the machine can do, detected again by this apply (novox/hq ADR 0161): the
|
||||||
|
// same shape enrolment sends, so a machine that gained or lost a capability — switched its
|
||||||
|
// network manager — is known at its next push and not at its next enrolment. Absent from a host
|
||||||
|
// older than this, and then the enrolment's profile stands.
|
||||||
|
Profile map[string]any `json:"profile,omitempty"`
|
||||||
// Reachable is what can be reached on the machine now: every listening socket and every
|
// 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.
|
// published container port. Only an adopted node reports it; it is what converging previews.
|
||||||
Reachable []Reach `json:"reachable,omitempty"`
|
Reachable []Reach `json:"reachable,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user