Write the facts snapshot in format 2, with each machine's grants and credential bindings (design 47 R4c-2, issue 1394)
Every reader takes format 2 since R4b (ADR 0456 clause 1), and the assigner's composition check reads the grants and bindings (ADR 0456 clause 2): the writer moves now, names only, never a sealed value.
This commit is contained in:
@@ -169,10 +169,10 @@ func factsStore(ctx context.Context, open *stores) (string, error) {
|
|||||||
return address, nil
|
return address, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writtenFormat is the format of the snapshot this controller writes: 1, the format its own reader knew before mesh-sdk's
|
// writtenFormat is the format of the snapshot this controller writes: mesh-sdk's go/facts current format, 2, which carries
|
||||||
// go/facts took the type over (issue 1390, novox/hq ADR 0456). The readers (the merge gate, the check) read formats 1 and
|
// each machine's grants and credential bindings besides everything format 1 held (issue 1394, novox/hq ADR 0456 clause 2).
|
||||||
// 2; the writer moves to 2, which also carries each machine's grants and bindings, in a later release of its own.
|
// Every reader (the merge gate, the build seat's check) reads formats 1 and 2.
|
||||||
const writtenFormat = 1
|
const writtenFormat = snapshot.Format
|
||||||
|
|
||||||
// gatherFacts composes one snapshot from the store: read only, nothing made, nothing sent.
|
// 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) {
|
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())})
|
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)
|
m.Declaration = declarationFacts(ctx, open, n.Name, gensErr, scrub)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return snapshot.Facts{}, ctx.Err()
|
return snapshot.Facts{}, ctx.Err()
|
||||||
|
|||||||
@@ -9,12 +9,11 @@ import (
|
|||||||
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
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
|
// 1394 (novox/hq ADR 0456 clause 2): the snapshot this controller writes is format 2, which carries each machine's grants and
|
||||||
// (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
|
// bindings, and the reader it shares with the build seat (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer.
|
||||||
// of its own. It failed first with the controller's own reader, which refused format 2 as newer than it knew.
|
func TestTheWriterWritesFormatTwoAndTheReaderTakesBothFormats(t *testing.T) {
|
||||||
func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
if writtenFormat != 2 {
|
||||||
if writtenFormat != 1 {
|
t.Fatalf("the writer writes format %d, not 2", writtenFormat)
|
||||||
t.Fatalf("the writer writes format %d, not 1", writtenFormat)
|
|
||||||
}
|
}
|
||||||
taken := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
|
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} {
|
for format, want := range map[int]bool{1: true, 2: true, 3: false} {
|
||||||
@@ -32,11 +31,23 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
|||||||
t.Errorf("format %d is not refused as newer: %v", format, err)
|
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.
|
// Format 2 carries a machine's grants and bindings, and they are encoded in one order whatever order they were made in.
|
||||||
body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6}}}).Encode()
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
var raw struct {
|
||||||
Format int `json:"facts"`
|
Format int `json:"facts"`
|
||||||
Machines []map[string]json.RawMessage `json:"machines"`
|
Machines []map[string]json.RawMessage `json:"machines"`
|
||||||
@@ -44,15 +55,14 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
|
|||||||
if err := json.Unmarshal(body, &raw); err != nil {
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if raw.Format != 1 {
|
if raw.Format != 2 {
|
||||||
t.Errorf("the snapshot says format %d", raw.Format)
|
t.Errorf("the snapshot says format %d", raw.Format)
|
||||||
}
|
}
|
||||||
for _, m := range raw.Machines {
|
for _, m := range raw.Machines {
|
||||||
if _, has := m["grants"]; has {
|
for _, key := range []string{"grants", "bindings"} {
|
||||||
t.Error("a format 1 snapshot carries grants")
|
if _, has := m[key]; !has {
|
||||||
|
t.Errorf("a format 2 snapshot carries no %s", key)
|
||||||
}
|
}
|
||||||
if _, has := m["bindings"]; has {
|
|
||||||
t.Error("a format 1 snapshot carries bindings")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
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
|
// 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.
|
// 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)
|
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
|
return open, secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +70,8 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if f.Format != 1 {
|
if f.Format != 2 {
|
||||||
t.Fatalf("the writer wrote format %d: it keeps format 1 until its own release moves it (issue 1390)", f.Format)
|
t.Fatalf("the writer wrote format %d, not 2 (issue 1394)", f.Format)
|
||||||
}
|
}
|
||||||
body, err := f.Encode()
|
body, err := f.Encode()
|
||||||
if err != nil {
|
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.
|
// What a check needs is there: every machine, its length, its modules, its declaration composing.
|
||||||
if len(f.Machines) != 2 || f.Longest() != len("laptop") {
|
if len(f.Machines) != 2 || f.Longest() != len("laptop") {
|
||||||
t.Fatalf("machines %+v, longest %d", f.Machines, f.Longest())
|
t.Fatalf("machines %+v, longest %d", f.Machines, f.Longest())
|
||||||
|
|||||||
Reference in New Issue
Block a user