Merge pull request 'Write the facts snapshot in format 2 with each machine's grants and credential bindings (design 47 R4c-2, issue 1394)' (#409) from feat/r4c2-new-keys into main
This commit was merged in pull request #409.
This commit is contained in:
@@ -169,10 +169,10 @@ func factsStore(ctx context.Context, open *stores) (string, error) {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// writtenFormat is the format of the snapshot this controller writes: 1, the format its own reader knew before mesh-sdk's
|
||||
// go/facts took the type over (issue 1390, novox/hq ADR 0456). The readers (the merge gate, the check) read formats 1 and
|
||||
// 2; the writer moves to 2, which also carries each machine's grants and bindings, in a later release of its own.
|
||||
const writtenFormat = 1
|
||||
// writtenFormat is the format of the snapshot this controller writes: mesh-sdk's go/facts current format, 2, which carries
|
||||
// each machine's grants and credential bindings besides everything format 1 held (issue 1394, novox/hq ADR 0456 clause 2).
|
||||
// Every reader (the merge gate, the build seat's check) reads formats 1 and 2.
|
||||
const writtenFormat = snapshot.Format
|
||||
|
||||
// gatherFacts composes one snapshot from the store: read only, nothing made, nothing sent.
|
||||
func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot.Facts, error) {
|
||||
@@ -367,6 +367,23 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
|
||||
m.Settings = append(m.Settings, snapshot.Settings{Module: module, Values: scrub.Values(layer.Whole())})
|
||||
}
|
||||
}
|
||||
// Who was issued a credential by this machine, by name only: never what the credential is, sealed or not.
|
||||
issued, err := inv.SecretsFrom(ctx, n.Name)
|
||||
if err != nil {
|
||||
return snapshot.Facts{}, err
|
||||
}
|
||||
for _, g := range issued {
|
||||
m.Grants = append(m.Grants, scrub.Grant(g.Name, g.Consumer, g.ConsumerModule, g.Local))
|
||||
}
|
||||
bound, err := inv.BindingsFor(ctx, n.Name)
|
||||
if err != nil {
|
||||
return snapshot.Facts{}, err
|
||||
}
|
||||
for consumer, byProvision := range bound {
|
||||
for provision, c := range byProvision {
|
||||
m.Bindings = append(m.Bindings, scrub.Binding(consumer, provision, c.Node, c.Module))
|
||||
}
|
||||
}
|
||||
m.Declaration = declarationFacts(ctx, open, n.Name, gensErr, scrub)
|
||||
if ctx.Err() != nil {
|
||||
return snapshot.Facts{}, ctx.Err()
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
||||
)
|
||||
|
||||
// 1390 (novox/hq ADR 0456): the snapshot this controller writes is format 1, and the reader it shares with the build seat
|
||||
// (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer than it knows. The writer moves to format 2 in a release
|
||||
// of its own. It failed first with the controller's own reader, which refused format 2 as newer than it knew.
|
||||
func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
||||
if writtenFormat != 1 {
|
||||
t.Fatalf("the writer writes format %d, not 1", writtenFormat)
|
||||
// 1394 (novox/hq ADR 0456 clause 2): the snapshot this controller writes is format 2, which carries each machine's grants and
|
||||
// bindings, and the reader it shares with the build seat (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer.
|
||||
func TestTheWriterWritesFormatTwoAndTheReaderTakesBothFormats(t *testing.T) {
|
||||
if writtenFormat != 2 {
|
||||
t.Fatalf("the writer writes format %d, not 2", writtenFormat)
|
||||
}
|
||||
taken := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
|
||||
for format, want := range map[int]bool{1: true, 2: true, 3: false} {
|
||||
@@ -32,10 +31,22 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
||||
t.Errorf("format %d is not refused as newer: %v", format, err)
|
||||
}
|
||||
}
|
||||
// What the writer composes carries no field of format 2: a reader of 1 and of 2 read it alike.
|
||||
body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6}}}).Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// Format 2 carries a machine's grants and bindings, and they are encoded in one order whatever order they were made in.
|
||||
grants := []snapshot.Grant{{Provision: "s3-bucket", ConsumerNode: "bbbbbb", ConsumerModule: "files"},
|
||||
{Provision: "s3-bucket", ConsumerNode: "aaaaaa", ConsumerModule: "files"}}
|
||||
bindings := []snapshot.Binding{{ConsumerModule: "files", Provision: "s3-bucket", ProviderNode: "cccccc", ProviderModule: "objects"}}
|
||||
encode := func(g []snapshot.Grant) []byte {
|
||||
body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6,
|
||||
Grants: g, Bindings: bindings}}}).Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
body := encode(grants)
|
||||
// 1394/AC3 (the bytes do not depend on the order the rows came in)
|
||||
if reversed := encode([]snapshot.Grant{grants[1], grants[0]}); string(reversed) != string(body) {
|
||||
t.Error("grants in another order encode to other bytes")
|
||||
}
|
||||
var raw struct {
|
||||
Format int `json:"facts"`
|
||||
@@ -44,15 +55,14 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if raw.Format != 1 {
|
||||
if raw.Format != 2 {
|
||||
t.Errorf("the snapshot says format %d", raw.Format)
|
||||
}
|
||||
for _, m := range raw.Machines {
|
||||
if _, has := m["grants"]; has {
|
||||
t.Error("a format 1 snapshot carries grants")
|
||||
}
|
||||
if _, has := m["bindings"]; has {
|
||||
t.Error("a format 1 snapshot carries bindings")
|
||||
for _, key := range []string{"grants", "bindings"} {
|
||||
if _, has := m[key]; !has {
|
||||
t.Errorf("a format 2 snapshot carries no %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// The facts snapshot (novox/hq to-be 45 §9): composed from the store, every machine under a pseudonym
|
||||
@@ -55,6 +56,11 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
|
||||
// Sealed, never kept plain (ADR 0004): the sealed blobs are what the store holds, and none may leave.
|
||||
secrets = append(secrets, s.ForConsumer, s.ForProvider)
|
||||
}
|
||||
// A binding recorded for the consumer on the laptop, to the provider on the anchor.
|
||||
if err := open.inventory.RecordBindings(ctx, "laptop", []inventory.Binding{{Machine: "laptop", Consumer: "files",
|
||||
Provision: "s3-bucket", Provider: catalogue.Chosen{Node: "anchor", Module: "objects"}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return open, secrets
|
||||
}
|
||||
|
||||
@@ -64,8 +70,8 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Format != 1 {
|
||||
t.Fatalf("the writer wrote format %d: it keeps format 1 until its own release moves it (issue 1390)", f.Format)
|
||||
if f.Format != 2 {
|
||||
t.Fatalf("the writer wrote format %d, not 2 (issue 1394)", f.Format)
|
||||
}
|
||||
body, err := f.Encode()
|
||||
if err != nil {
|
||||
@@ -89,6 +95,20 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1394/AC1: the provider's machine lists the grant, the consumer's machine the binding, machine names scrubbed.
|
||||
anchorFacts, _ := f.Machine(snapshot.Pseudonym("machine", "anchor"))
|
||||
laptopFacts, _ := f.Machine(snapshot.Pseudonym("machine", "laptop"))
|
||||
wantGrant := snapshot.Grant{Provision: "s3-bucket", ConsumerNode: snapshot.Pseudonym("machine", "laptop"), ConsumerModule: "files"}
|
||||
if len(anchorFacts.Grants) != 1 || anchorFacts.Grants[0] != wantGrant {
|
||||
t.Errorf("the anchor's grants read as %+v, want %+v", anchorFacts.Grants, wantGrant)
|
||||
}
|
||||
wantBinding := snapshot.Binding{ConsumerModule: "files", Provision: "s3-bucket",
|
||||
ProviderNode: snapshot.Pseudonym("machine", "anchor"), ProviderModule: "objects"}
|
||||
if len(laptopFacts.Bindings) != 1 || laptopFacts.Bindings[0] != wantBinding {
|
||||
t.Errorf("the laptop's bindings read as %+v, want %+v", laptopFacts.Bindings, wantBinding)
|
||||
}
|
||||
// 1394/AC2: the text scan above ran over a snapshot that holds those rows, so no sealed value is among them.
|
||||
|
||||
// What a check needs is there: every machine, its length, its modules, its declaration composing.
|
||||
if len(f.Machines) != 2 || f.Longest() != len("laptop") {
|
||||
t.Fatalf("machines %+v, longest %d", f.Machines, f.Longest())
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
|
||||
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
|
||||
//
|
||||
// The four manifests (the watchman's is the fourth, issue 1329) are copied from mesh-delivery's main at 9c15112d935a
|
||||
// (modules/<module>/module.json, read 2026-10-20; the first three are unchanged since 6a23627426c6), whole, so a change
|
||||
// to one of them there that changes its grant is a change to read here.
|
||||
// The four manifests (the watchman's is the fourth, issue 1329): the bridge's and the indexer's are copied whole from
|
||||
// mesh-delivery's branch feat/r4c2-new-keys at b7870ac9a6386467318454e9d5d4790987eb708d (modules/<module>/module.json,
|
||||
// read 2026-10-20; issues 1448 and 1449 add the bridge's gitea_get_file and performed bucket and the indexer's manifests
|
||||
// bucket), the teller's and the watchman's from mesh-delivery's main at 9c15112d935a, whole, so a change to one of them
|
||||
// there that changes its grant is a change to read here.
|
||||
const bridgeManifest = `{
|
||||
"module": "mesh-delivery-forge-bridge",
|
||||
"version": "1",
|
||||
@@ -29,7 +31,8 @@ const bridgeManifest = `{
|
||||
{ "name": "forge", "events": ["forge.**"] }
|
||||
],
|
||||
"state": [
|
||||
{ "name": "heads", "history": 1 }
|
||||
{ "name": "heads", "history": 1 },
|
||||
{ "name": "performed", "history": 1 }
|
||||
],
|
||||
"reads": [
|
||||
"mesh-delivery-gate.merges",
|
||||
@@ -42,7 +45,8 @@ const bridgeManifest = `{
|
||||
"gitea.gitea_note_append",
|
||||
"gitea.gitea_add_comment",
|
||||
"gitea.gitea_open_pulls",
|
||||
"gitea.gitea_commit_trees"
|
||||
"gitea.gitea_commit_trees",
|
||||
"gitea.gitea_get_file"
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "${dir:state}/broker"
|
||||
@@ -104,6 +108,7 @@ const indexerManifest = `{
|
||||
],
|
||||
"state": [
|
||||
{ "name": "candidates", "history": 10 },
|
||||
{ "name": "manifests", "history": 1 },
|
||||
{ "name": "modules", "history": 20 }
|
||||
],
|
||||
"reads": [
|
||||
@@ -437,8 +442,8 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
|
||||
all := composed(t, partsRecords(t))
|
||||
bridge, indexer, teller := all["mesh-delivery-forge-bridge@novox"], all["mesh-delivery-indexer@novox"], all["mesh-delivery-teller@novox"]
|
||||
|
||||
// The forge bridge: state heads, the stream FORGE, reads merges and owed, invokes seven tools of the forge.
|
||||
if got, want := withPrefix(bridge.Publish, "$KV."), []string{"$KV.mesh-delivery-forge-bridge_heads.>"}; !slices.Equal(got, want) {
|
||||
// The forge bridge: state heads and performed, the stream FORGE, reads merges and owed, invokes eight tools of the forge.
|
||||
if got, want := withPrefix(bridge.Publish, "$KV."), []string{"$KV.mesh-delivery-forge-bridge_heads.>", "$KV.mesh-delivery-forge-bridge_performed.>"}; !slices.Equal(got, want) {
|
||||
t.Errorf("the bridge writes %v; want exactly %v", got, want)
|
||||
}
|
||||
if got, want := eventsSaid(bridge), []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.>"}; !slices.Equal(got, want) {
|
||||
@@ -458,7 +463,7 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
|
||||
t.Error("the bridge hears an event of gitea's it does not consume")
|
||||
}
|
||||
called := []string{"gitea_add_comment", "gitea_commit_status", "gitea_commit_trees", "gitea_delivery_view",
|
||||
"gitea_merge_pull_request", "gitea_note_append", "gitea_open_pulls"}
|
||||
"gitea_get_file", "gitea_merge_pull_request", "gitea_note_append", "gitea_open_pulls"}
|
||||
if got := toolsCalled(bridge, "gitea"); !slices.Equal(got, called) {
|
||||
t.Errorf("the bridge calls %v; want exactly %v", got, called)
|
||||
}
|
||||
@@ -466,8 +471,8 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
|
||||
t.Error("the bridge may call a tool of gitea's it does not invoke")
|
||||
}
|
||||
|
||||
// The indexer: state candidates and modules, reads the controller's builds and the gate's source, hears FORGE.
|
||||
if got, want := withPrefix(indexer.Publish, "$KV."), []string{"$KV.mesh-delivery-indexer_candidates.>", "$KV.mesh-delivery-indexer_modules.>"}; !slices.Equal(got, want) {
|
||||
// The indexer: state candidates, manifests and modules, reads the controller's builds and the gate's source, hears FORGE.
|
||||
if got, want := withPrefix(indexer.Publish, "$KV."), []string{"$KV.mesh-delivery-indexer_candidates.>", "$KV.mesh-delivery-indexer_manifests.>", "$KV.mesh-delivery-indexer_modules.>"}; !slices.Equal(got, want) {
|
||||
t.Errorf("the indexer writes %v; want exactly %v", got, want)
|
||||
}
|
||||
if got := eventsSaid(indexer); len(got) != 0 {
|
||||
@@ -509,7 +514,7 @@ func TestNoPartsCredentialReachesBeyondItsDeclaration(t *testing.T) {
|
||||
all := composed(t, partsRecords(t))
|
||||
parts := []string{"mesh-delivery-forge-bridge", "mesh-delivery-indexer", "mesh-delivery-teller"}
|
||||
buckets := map[string][]string{
|
||||
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
|
||||
"mesh-delivery-forge-bridge": {"heads", "performed"}, "mesh-delivery-indexer": {"candidates", "manifests", "modules"},
|
||||
"mesh-delivery-teller": {"answers"}, "mesh-delivery-gate": {"merges", "owed", "source", "asks", "deliveries"},
|
||||
"mesh-controller": {"builds", "plans"},
|
||||
}
|
||||
@@ -570,7 +575,7 @@ func TestTheToolRunnerCarriesNoneOfTheDeliverysParts(t *testing.T) {
|
||||
}}})
|
||||
runner := all["runner@novox"]
|
||||
buckets := map[string][]string{
|
||||
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
|
||||
"mesh-delivery-forge-bridge": {"heads", "performed"}, "mesh-delivery-indexer": {"candidates", "manifests", "modules"},
|
||||
"mesh-delivery-teller": {"answers"}, "mesh-delivery-watchman": {"presence", "pulse", "stuck"},
|
||||
}
|
||||
for part, names := range buckets {
|
||||
|
||||
Reference in New Issue
Block a user