Merge pull request 'Grant a bar one key of the state it shows, and per-machine state its own machine's key (hq ADR 0260)' (#151) from fix/state-grants-per-key into main
This commit was merged in pull request #151.
This commit is contained in:
@@ -108,7 +108,7 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
|
|||||||
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
|
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.State = stateIssuedFor(d)
|
m.State = stateIssuedFor(d, node)
|
||||||
if len(d.Invokes) > 0 {
|
if len(d.Invokes) > 0 {
|
||||||
m.Reaches = map[string][]string{}
|
m.Reaches = map[string][]string{}
|
||||||
for _, t := range d.Invokes {
|
for _, t := range d.Invokes {
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ type Principal struct {
|
|||||||
// instances and read by whoever declares it.
|
// instances and read by whoever declares it.
|
||||||
State []string
|
State []string
|
||||||
Reads []string
|
Reads []string
|
||||||
|
// PerMachine is the local names of its state keyed by machine, reaching the key named for Node and
|
||||||
|
// no other; KeyedReads the keys of others' state it reads one key at a time (novox/hq ADR 0260).
|
||||||
|
PerMachine []string
|
||||||
|
KeyedReads []KeyedRead
|
||||||
|
|
||||||
// SnapshotsTheBus is the bus's own module, the one holding mesh-broker (novox/hq ADR 0235). Its
|
// SnapshotsTheBus is the bus's own module, the one holding mesh-broker (novox/hq ADR 0235). Its
|
||||||
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
|
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
|
||||||
@@ -591,7 +595,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
|
|
||||||
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
|
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
|
||||||
// watched, its own written too.
|
// watched, its own written too.
|
||||||
pub = append(pub, stateGrants(p.Module, p.State, p.Reads)...)
|
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State,
|
||||||
|
PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...)
|
||||||
|
|
||||||
case KindNodeTools:
|
case KindNodeTools:
|
||||||
// **One process serves what every module on the machine would have served for itself**
|
// **One process serves what every module on the machine would have served for itself**
|
||||||
@@ -667,7 +672,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
// does not write another's bucket through it is the runtime's to keep, from the membership
|
// does not write another's bucket through it is the runtime's to keep, from the membership
|
||||||
// each assignment is issued, as it keeps each module's events under that module's own name.
|
// each assignment is issued, as it keeps each module's events under that module's own name.
|
||||||
for _, d := range p.Carries {
|
for _, d := range p.Carries {
|
||||||
pub = append(pub, stateGrants(d.Module, stateNames(d.State), d.Reads)...)
|
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
|
||||||
|
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...)
|
||||||
}
|
}
|
||||||
sub = unique(sub)
|
sub = unique(sub)
|
||||||
pub = unique(pub)
|
pub = unique(pub)
|
||||||
|
|||||||
+89
-14
@@ -32,6 +32,9 @@ type Bucket struct {
|
|||||||
History int
|
History int
|
||||||
// TTLSeconds is how long a value lives; zero is until replaced or deleted.
|
// TTLSeconds is how long a value lives; zero is until replaced or deleted.
|
||||||
TTLSeconds int
|
TTLSeconds int
|
||||||
|
// PerMachine says each machine's instance writes only the key named for its machine (novox/hq ADR
|
||||||
|
// 0260), and is granted that key and no other.
|
||||||
|
PerMachine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// BucketName is the bucket a module's state lives in: the module and the local name joined by an
|
// BucketName is the bucket a module's state lives in: the module and the local name joined by an
|
||||||
@@ -72,52 +75,124 @@ func bucketOfRead(read string) (string, bool) {
|
|||||||
// read; without CONSUMER.CREATE no key is listed and nothing is watched; without CONSUMER.DELETE a
|
// read; without CONSUMER.CREATE no key is listed and nothing is watched; without CONSUMER.DELETE a
|
||||||
// watch cannot be stopped and lingers on the server. A write outside these is refused by the server
|
// watch cannot be stopped and lingers on the server. A write outside these is refused by the server
|
||||||
// — and reaches the writer as a timeout, not a refusal, which is why the runtime refuses first.
|
// — and reaches the writer as a timeout, not a refusal, which is why the runtime refuses first.
|
||||||
func stateGrants(module string, keeps []string, reads []string) []string {
|
//
|
||||||
|
// **One key, where the state is per machine** (novox/hq ADR 0260). The owner's instance on a machine
|
||||||
|
// writes and reads the key named for that machine, and a holder granted a read for its machine reads
|
||||||
|
// that key: a direct get of the key's own subject, and a consumer whose filter is that subject — the
|
||||||
|
// server's create-with-filter form, which a client uses for a watch of one key and which the server
|
||||||
|
// holds to the filter it names. What cannot be narrowed by key: binding (STREAM.INFO), deleting a
|
||||||
|
// consumer (named at random by the client) and flow control; none of them reads or writes a value.
|
||||||
|
func stateGrants(a stateAccess) []string {
|
||||||
var out []string
|
var out []string
|
||||||
read := func(bucket string) {
|
read := func(bucket, key string) {
|
||||||
stream := "KV_" + bucket
|
stream := "KV_" + bucket
|
||||||
|
out = append(out, "$JS.API.STREAM.INFO."+stream)
|
||||||
|
if key == "" {
|
||||||
|
out = append(out,
|
||||||
|
"$JS.API.DIRECT.GET."+stream+".>",
|
||||||
|
"$JS.API.CONSUMER.CREATE."+stream+".>")
|
||||||
|
} else {
|
||||||
|
out = append(out,
|
||||||
|
"$JS.API.DIRECT.GET."+stream+".$KV."+bucket+"."+key,
|
||||||
|
"$JS.API.CONSUMER.CREATE."+stream+".*.$KV."+bucket+"."+key)
|
||||||
|
}
|
||||||
out = append(out,
|
out = append(out,
|
||||||
"$JS.API.STREAM.INFO."+stream,
|
|
||||||
"$JS.API.DIRECT.GET."+stream+".>",
|
|
||||||
"$JS.API.CONSUMER.CREATE."+stream+".>",
|
|
||||||
"$JS.API.CONSUMER.DELETE."+stream+".>",
|
"$JS.API.CONSUMER.DELETE."+stream+".>",
|
||||||
"$JS.FC."+stream+".>")
|
"$JS.FC."+stream+".>")
|
||||||
}
|
}
|
||||||
for _, name := range keeps {
|
machineKey := ""
|
||||||
|
if safeSubject.MatchString(a.Node) {
|
||||||
|
machineKey = a.Node
|
||||||
|
}
|
||||||
|
perMachine := map[string]bool{}
|
||||||
|
for _, n := range a.PerMachine {
|
||||||
|
perMachine[n] = true
|
||||||
|
}
|
||||||
|
for _, name := range a.Keeps {
|
||||||
if !safeSubject.MatchString(name) {
|
if !safeSubject.MatchString(name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bucket := BucketName(module, name)
|
bucket := BucketName(a.Module, name)
|
||||||
read(bucket)
|
if perMachine[name] {
|
||||||
|
if machineKey == "" {
|
||||||
|
continue // a machine with no usable name reaches none of it
|
||||||
|
}
|
||||||
|
read(bucket, machineKey)
|
||||||
|
out = append(out, "$KV."+bucket+"."+machineKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
read(bucket, "")
|
||||||
out = append(out, "$KV."+bucket+".>")
|
out = append(out, "$KV."+bucket+".>")
|
||||||
}
|
}
|
||||||
for _, r := range reads {
|
for _, r := range a.Reads {
|
||||||
if bucket, ok := bucketOfRead(r); ok {
|
if bucket, ok := bucketOfRead(r); ok {
|
||||||
read(bucket)
|
read(bucket, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range a.KeyedReads {
|
||||||
|
if bucket, ok := bucketOfRead(k.Read); ok && safeSubject.MatchString(k.Key) {
|
||||||
|
read(bucket, k.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyedRead is one key of another module's state a principal reads, and no other key of it.
|
||||||
|
type KeyedRead struct {
|
||||||
|
Read string `json:"read"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateAccess is what one principal may do with state: the module's own buckets (some per machine),
|
||||||
|
// what it reads whole, and what it reads for its machine's key alone.
|
||||||
|
type stateAccess struct {
|
||||||
|
Module, Node string
|
||||||
|
Keeps, PerMachine []string
|
||||||
|
Reads []string
|
||||||
|
KeyedReads []KeyedRead
|
||||||
|
}
|
||||||
|
|
||||||
|
// perMachineNames is the local names of a module's buckets keyed by machine.
|
||||||
|
func perMachineNames(buckets []Bucket) []string {
|
||||||
|
var out []string
|
||||||
|
for _, b := range buckets {
|
||||||
|
if b.PerMachine {
|
||||||
|
out = append(out, b.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateIssued is one bucket an assignment may reach, by the name its module uses for it: its own
|
// StateIssued is one bucket an assignment may reach, by the name its module uses for it: its own
|
||||||
// state by the local name, another's as `<module>.<name>` (novox/hq ADR 0201).
|
// state by the local name, another's as `<module>.<name>` (novox/hq ADR 0201). Key, when set, is the
|
||||||
|
// one key it may reach (novox/hq ADR 0260): this machine's.
|
||||||
type StateIssued struct {
|
type StateIssued struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket"`
|
||||||
Writes bool `json:"writes,omitempty"`
|
Writes bool `json:"writes,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// stateIssuedFor is every bucket a module's code may reach, as its membership lists them.
|
// stateIssuedFor is every bucket a module's code may reach on a machine, as its membership lists them.
|
||||||
func stateIssuedFor(d Declared) []StateIssued {
|
func stateIssuedFor(d Declared, node string) []StateIssued {
|
||||||
var out []StateIssued
|
var out []StateIssued
|
||||||
for _, b := range d.State {
|
for _, b := range d.State {
|
||||||
out = append(out, StateIssued{Name: b.Name, Bucket: BucketName(d.Module, b.Name), Writes: true})
|
issued := StateIssued{Name: b.Name, Bucket: BucketName(d.Module, b.Name), Writes: true}
|
||||||
|
if b.PerMachine {
|
||||||
|
issued.Key = node
|
||||||
|
}
|
||||||
|
out = append(out, issued)
|
||||||
}
|
}
|
||||||
for _, r := range d.Reads {
|
for _, r := range d.Reads {
|
||||||
if bucket, ok := bucketOfRead(r); ok {
|
if bucket, ok := bucketOfRead(r); ok {
|
||||||
out = append(out, StateIssued{Name: r, Bucket: bucket})
|
out = append(out, StateIssued{Name: r, Bucket: bucket})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, k := range d.KeyedReads {
|
||||||
|
if bucket, ok := bucketOfRead(k.Read); ok {
|
||||||
|
out = append(out, StateIssued{Name: k.Read, Bucket: bucket, Key: k.Key})
|
||||||
|
}
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ func TestAModuleWithNoStateReachesNoBucket(t *testing.T) {
|
|||||||
|
|
||||||
// A read that names no bucket grants nothing rather than something that happens to parse.
|
// A read that names no bucket grants nothing rather than something that happens to parse.
|
||||||
func TestAReadThatNamesNoBucketGrantsNothing(t *testing.T) {
|
func TestAReadThatNamesNoBucketGrantsNothing(t *testing.T) {
|
||||||
if got := stateGrants("a", nil, []string{"nodot", "x.", ".y", "a.b>"}); len(got) != 0 {
|
if got := stateGrants(stateAccess{Module: "a", Node: "one", Reads: []string{"nodot", "x.", ".y", "a.b>"},
|
||||||
|
KeyedReads: []KeyedRead{{Read: "nodot", Key: "one"}, {Read: "a.b", Key: "x>"}}}); len(got) != 0 {
|
||||||
t.Fatalf("granted %v for reads that name no bucket", got)
|
t.Fatalf("granted %v for reads that name no bucket", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,3 +179,58 @@ func TestABucketIsAssertedOverAHeldConnection(t *testing.T) {
|
|||||||
t.Fatalf("the bucket is not there: %v", err)
|
t.Fatalf("the bucket is not there: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **State keyed by machine reaches its machine's key alone** (novox/hq ADR 0260): the owner's instance on
|
||||||
|
// a machine writes and reads that key, and a bar granted a key reads that key — a direct get of its
|
||||||
|
// subject and a consumer filtered to it — and no other key of the bucket.
|
||||||
|
func TestPerMachineStateAndAKeyedReadReachOneKey(t *testing.T) {
|
||||||
|
owner, err := PermissionsFor(Principal{Kind: KindModule, Node: "laptop", Module: "power", PasswordHash: "x",
|
||||||
|
State: []string{"draw"}, PerMachine: []string{"draw"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
has(t, owner.Publish, "$KV.power_draw.laptop")
|
||||||
|
has(t, owner.Publish, "$JS.API.DIRECT.GET.KV_power_draw.$KV.power_draw.laptop")
|
||||||
|
has(t, owner.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.*.$KV.power_draw.laptop")
|
||||||
|
hasNot(t, owner.Publish, "$KV.power_draw.>")
|
||||||
|
hasNot(t, owner.Publish, "$JS.API.DIRECT.GET.KV_power_draw.>")
|
||||||
|
hasNot(t, owner.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.>")
|
||||||
|
|
||||||
|
bar, err := PermissionsFor(Principal{Kind: KindModule, Node: "laptop", Module: "bar", PasswordHash: "x",
|
||||||
|
KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
has(t, bar.Publish, "$JS.API.STREAM.INFO.KV_power_draw")
|
||||||
|
has(t, bar.Publish, "$JS.API.DIRECT.GET.KV_power_draw.$KV.power_draw.laptop")
|
||||||
|
has(t, bar.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.*.$KV.power_draw.laptop")
|
||||||
|
for _, s := range bar.Publish {
|
||||||
|
if strings.HasPrefix(s, "$KV.") || strings.HasSuffix(s, "KV_power_draw.>") && !strings.Contains(s, "DELETE") && !strings.HasPrefix(s, "$JS.FC.") {
|
||||||
|
t.Fatalf("a keyed read was granted %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the machine's runtime, which carries both, is granted the union: still the one key.
|
||||||
|
runtime, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule, PasswordHash: "x",
|
||||||
|
Carries: []Declared{
|
||||||
|
{Module: "power", State: []Bucket{{Module: "power", Name: "draw", PerMachine: true}}},
|
||||||
|
{Module: "bar", KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
has(t, runtime.Publish, "$KV.power_draw.laptop")
|
||||||
|
hasNot(t, runtime.Publish, "$KV.power_draw.>")
|
||||||
|
hasNot(t, runtime.Publish, "$JS.API.DIRECT.GET.KV_power_draw.>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAMembershipNamesTheOneKeyOfKeyedState(t *testing.T) {
|
||||||
|
m := MembershipFor("laptop", Declared{Module: "bar", KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}}, Placements{})
|
||||||
|
if len(m.State) != 1 || m.State[0] != (StateIssued{Name: "power.draw", Bucket: "power_draw", Key: "laptop"}) {
|
||||||
|
t.Fatalf("%+v", m.State)
|
||||||
|
}
|
||||||
|
o := MembershipFor("laptop", Declared{Module: "power", State: []Bucket{{Module: "power", Name: "draw", PerMachine: true}}}, Placements{})
|
||||||
|
if len(o.State) != 1 || o.State[0] != (StateIssued{Name: "draw", Bucket: "power_draw", Writes: true, Key: "laptop"}) {
|
||||||
|
t.Fatalf("%+v", o.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ type Declared struct {
|
|||||||
State []Bucket
|
State []Bucket
|
||||||
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0201).
|
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0201).
|
||||||
Reads []string
|
Reads []string
|
||||||
|
// KeyedReads are keys of other modules' state it reads, each one key alone: what a seat's holder is
|
||||||
|
// granted for the pieces the modules beside it offer (novox/hq ADR 0260).
|
||||||
|
KeyedReads []KeyedRead
|
||||||
// NoAccount says the module declares no own secret named broker, so no account could ever be
|
// NoAccount says the module declares no own secret named broker, so no account could ever be
|
||||||
// delivered to it and nothing can connect as it (novox/hq issue 195). Said in the negative so a
|
// delivered to it and nothing can connect as it (novox/hq issue 195). Said in the negative so a
|
||||||
// record that does not say is composed as it always was.
|
// record that does not say is composed as it always was.
|
||||||
@@ -113,6 +116,7 @@ func Users(r Records) ([]Principal, error) {
|
|||||||
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
||||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
||||||
State: stateNames(d.State), Reads: d.Reads, SnapshotsTheBus: d.SnapshotsTheBus,
|
State: stateNames(d.State), Reads: d.Reads, SnapshotsTheBus: d.SnapshotsTheBus,
|
||||||
|
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if runtimeHere {
|
if runtimeHere {
|
||||||
|
|||||||
@@ -136,16 +136,18 @@ var barShows = map[string]map[string]barOption{
|
|||||||
"state": {"state": {kind: "text", required: true}, "key": {kind: "text"}},
|
"state": {"state": {kind: "text", required: true}, "key": {kind: "text"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// barBlockReads is the state a block shows, if any.
|
// barBlockReads is the state and key a block shows, if any; an empty key is the machine's own.
|
||||||
func barBlockReads(data map[string]any) []string {
|
func barBlockReads(data map[string]any) []StateRead {
|
||||||
if data["shows"] != "state" {
|
if data["shows"] != "state" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
options, _ := data["options"].(map[string]any)
|
options, _ := data["options"].(map[string]any)
|
||||||
if s, ok := options["state"].(string); ok && s != "" {
|
s, ok := options["state"].(string)
|
||||||
return []string{s}
|
if !ok || s == "" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
key, _ := options["key"].(string)
|
||||||
|
return []StateRead{{Read: s, Key: key}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type barOption struct {
|
type barOption struct {
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ type Shape struct {
|
|||||||
// Examples are pieces every holder's template must render, one for each variant the shape has,
|
// Examples are pieces every holder's template must render, one for each variant the shape has,
|
||||||
// so a holder that cannot render one is refused at registration and not found on a machine.
|
// so a holder that cannot render one is refused at registration and not found on a machine.
|
||||||
Examples []map[string]any
|
Examples []map[string]any
|
||||||
// Reads is the state on the bus a piece shows, as `<module>.<name>`, or nothing (novox/hq ADR
|
// Reads is the state on the bus a piece shows, each a state and the one key of it, or nothing
|
||||||
// 0255): a contributor offers only state it keeps itself, and the holder on the same machine is
|
// (novox/hq ADR 0260): a contributor offers only state it keeps itself, and the holder on the same
|
||||||
// granted to read it, so the holder's manifest names no contributor.
|
// machine is granted to read that key, so the holder's manifest names no contributor.
|
||||||
Reads func(data map[string]any) []string `json:"-"`
|
Reads func(data map[string]any) []StateRead `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaceField is one field a holder places by, and the values it takes.
|
// PlaceField is one field a holder places by, and the values it takes.
|
||||||
@@ -120,25 +120,38 @@ func ownStateProblems(m Manifest, i int, r Receivable, c SeatContribution) []str
|
|||||||
keeps[s.Name] = true
|
keeps[s.Name] = true
|
||||||
}
|
}
|
||||||
var problems []string
|
var problems []string
|
||||||
for _, read := range r.Shape.Reads(c.Data) {
|
for _, sr := range r.Shape.Reads(c.Data) {
|
||||||
module, local, err := ReadState(read)
|
module, local, err := ReadState(sr.Read)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
problems = append(problems, fmt.Sprintf("%s's contribution %d shows state %v", m.Module, i+1, err))
|
problems = append(problems, fmt.Sprintf("%s's contribution %d shows state %v", m.Module, i+1, err))
|
||||||
case module != m.Module || !keeps[local]:
|
case module != m.Module || !keeps[local]:
|
||||||
problems = append(problems, fmt.Sprintf("%s's contribution %d shows the state %s, which %s does not keep; a "+
|
problems = append(problems, fmt.Sprintf("%s's contribution %d shows the state %s, which %s does not keep; a "+
|
||||||
"module offers its own state only (novox/hq ADR 0255)", m.Module, i+1, read, m.Module))
|
"module offers its own state only (novox/hq ADR 0260)", m.Module, i+1, sr.Read, m.Module))
|
||||||
|
}
|
||||||
|
if sr.Key != "" && !stateKeyName.MatchString(sr.Key) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's contribution %d shows the key %q; a key is lower-case letters, "+
|
||||||
|
"digits and hyphens", m.Module, i+1, sr.Key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateRead is one key of another module's state that a holder reads: Read is `<module>.<name>`,
|
||||||
|
// and Key the one key, empty for the holder's own machine until ReadsGranted names it.
|
||||||
|
type StateRead struct {
|
||||||
|
Read, Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateKeyName is a key a piece may name: a machine's name is one.
|
||||||
|
var stateKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||||||
|
|
||||||
// ReadsGranted is the state a holder reads because modules on its machine offer it in pieces of a
|
// ReadsGranted is the state a holder reads because modules on its machine offer it in pieces of a
|
||||||
// kind its seat receives as data (novox/hq ADR 0255), sorted and once each. The bus grants the holder
|
// kind its seat receives as data (novox/hq ADR 0260), each with the one key the piece shows — the
|
||||||
// these beside its own `reads`: the holder names no contributor, and reads only what is offered to it
|
// machine's own when it names none — sorted and once each. The bus grants the holder these keys and
|
||||||
// on the machine it runs on.
|
// no others: the holder names no contributor, and reads only what is offered to it where it runs.
|
||||||
func ReadsGranted(holder Manifest, onMachine []Manifest) []string {
|
func ReadsGranted(holder Manifest, onMachine []Manifest, machine string) []StateRead {
|
||||||
seen := map[string]bool{}
|
seen := map[StateRead]bool{}
|
||||||
for _, claim := range holder.Claims {
|
for _, claim := range holder.Claims {
|
||||||
s, known := SeatNamed(claim.Name)
|
s, known := SeatNamed(claim.Name)
|
||||||
if !known {
|
if !known {
|
||||||
@@ -153,16 +166,30 @@ func ReadsGranted(holder Manifest, onMachine []Manifest) []string {
|
|||||||
if cs, ok := SeatNamed(c.Seat); !ok || cs.Name != s.Name || c.Kind != r.Kind {
|
if cs, ok := SeatNamed(c.Seat); !ok || cs.Name != s.Name || c.Kind != r.Kind {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, read := range r.Shape.Reads(c.Data) {
|
for _, sr := range r.Shape.Reads(c.Data) {
|
||||||
if module, _, err := ReadState(read); err == nil && module == m.Module && module != holder.Module {
|
if sr.Key == "" {
|
||||||
seen[read] = true
|
sr.Key = machine
|
||||||
|
}
|
||||||
|
if module, _, err := ReadState(sr.Read); err == nil && module == m.Module && module != holder.Module &&
|
||||||
|
stateKeyName.MatchString(sr.Key) {
|
||||||
|
seen[sr] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sortedKeys(seen)
|
out := make([]StateRead, 0, len(seen))
|
||||||
|
for sr := range seen {
|
||||||
|
out = append(out, sr)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
if out[i].Read != out[j].Read {
|
||||||
|
return out[i].Read < out[j].Read
|
||||||
|
}
|
||||||
|
return out[i].Key < out[j].Key
|
||||||
|
})
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
|
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
|
||||||
|
|||||||
@@ -364,14 +364,15 @@ func TestAStateBlockIsRenderedForThisMachineAndTheHolderIsGrantedToReadIt(t *tes
|
|||||||
if bar := composedFile(t, r, "a-bar.bottom"); !strings.Contains(bar, `command = "show power.draw laptop"`) {
|
if bar := composedFile(t, r, "a-bar.bottom"); !strings.Contains(bar, `command = "show power.draw laptop"`) {
|
||||||
t.Fatalf("the block does not read this machine's key:\n%s", bar)
|
t.Fatalf("the block does not read this machine's key:\n%s", bar)
|
||||||
}
|
}
|
||||||
if got := ReadsGranted(barHolder(), []Manifest{barHolder(), power}); len(got) != 1 || got[0] != "power.draw" {
|
if got := ReadsGranted(barHolder(), []Manifest{barHolder(), power}, "laptop"); len(got) != 1 ||
|
||||||
|
got[0] != (StateRead{Read: "power.draw", Key: "laptop"}) {
|
||||||
t.Fatalf("the holder is granted %v", got)
|
t.Fatalf("the holder is granted %v", got)
|
||||||
}
|
}
|
||||||
// Only the holder, and only for what the modules on its machine offer.
|
// Only the holder, and only for what the modules on its machine offer.
|
||||||
if got := ReadsGranted(power, []Manifest{barHolder(), power}); len(got) != 0 {
|
if got := ReadsGranted(power, []Manifest{barHolder(), power}, "laptop"); len(got) != 0 {
|
||||||
t.Fatalf("a contributor is granted %v", got)
|
t.Fatalf("a contributor is granted %v", got)
|
||||||
}
|
}
|
||||||
if got := ReadsGranted(barHolder(), []Manifest{barHolder()}); len(got) != 0 {
|
if got := ReadsGranted(barHolder(), []Manifest{barHolder()}, "laptop"); len(got) != 0 {
|
||||||
t.Fatalf("a holder alone is granted %v", got)
|
t.Fatalf("a holder alone is granted %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,3 +394,14 @@ func TestAModuleOffersItsOwnStateOnly(t *testing.T) {
|
|||||||
t.Errorf("its own state was refused: %v", err)
|
t.Errorf("its own state was refused: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPerMachineStateIsDeclaredAndKeptInItsLongForm(t *testing.T) {
|
||||||
|
m, err := ParseManifest([]byte(`{"module":"power","state":[{"name":"draw","ttl-seconds":30,"per-machine":true}]}`))
|
||||||
|
if err != nil || len(m.State) != 1 || !m.State[0].PerMachine {
|
||||||
|
t.Fatalf("%+v %v", m.State, err)
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(StateDeclaration{Name: "draw", PerMachine: true})
|
||||||
|
if string(raw) != `{"name":"draw","per-machine":true}` {
|
||||||
|
t.Fatalf("%s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ type StateDeclaration struct {
|
|||||||
History int `json:"history,omitempty"`
|
History int `json:"history,omitempty"`
|
||||||
// TTLSeconds is how long a value lives once written; zero is until it is replaced or deleted.
|
// TTLSeconds is how long a value lives once written; zero is until it is replaced or deleted.
|
||||||
TTLSeconds int `json:"ttl-seconds,omitempty"`
|
TTLSeconds int `json:"ttl-seconds,omitempty"`
|
||||||
|
// PerMachine says the state is keyed by machine: each machine's instance writes the key named for
|
||||||
|
// its machine and no other, and the bus grants it that key alone (novox/hq ADR 0260).
|
||||||
|
PerMachine bool `json:"per-machine,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds}.
|
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds}.
|
||||||
@@ -46,7 +49,7 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
|
|||||||
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
if err := dec.Decode(&full); err != nil {
|
if err := dec.Decode(&full); err != nil {
|
||||||
return fmt.Errorf("a state is either a name or {name, history, ttl-seconds}: %w", err)
|
return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine}: %w", err)
|
||||||
}
|
}
|
||||||
*s = StateDeclaration(full)
|
*s = StateDeclaration(full)
|
||||||
return nil
|
return nil
|
||||||
@@ -54,7 +57,7 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
|
|||||||
|
|
||||||
// MarshalJSON writes back the short form when there is nothing else to say.
|
// MarshalJSON writes back the short form when there is nothing else to say.
|
||||||
func (s StateDeclaration) MarshalJSON() ([]byte, error) {
|
func (s StateDeclaration) MarshalJSON() ([]byte, error) {
|
||||||
if s.History == 0 && s.TTLSeconds == 0 {
|
if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine {
|
||||||
return json.Marshal(s.Name)
|
return json.Marshal(s.Name)
|
||||||
}
|
}
|
||||||
type plain StateDeclaration
|
type plain StateDeclaration
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestAStateNameIsLocalAndOneToken(t *testing.T) {
|
|||||||
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
|
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
|
||||||
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
|
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
|
||||||
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
|
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
|
||||||
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds}`},
|
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine}`},
|
||||||
} {
|
} {
|
||||||
_, err := ParseManifest([]byte(c.manifest))
|
_, err := ParseManifest([]byte(c.manifest))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
|||||||
}
|
}
|
||||||
d := declaredFor(m, seats)
|
d := declaredFor(m, seats)
|
||||||
// And the state offered to it as a seat's holder by the modules beside it (novox/hq ADR 0255).
|
// And the state offered to it as a seat's holder by the modules beside it (novox/hq ADR 0255).
|
||||||
if granted := catalogue.ReadsGranted(m, onMachine(declared, modules)); len(granted) > 0 {
|
// For this machine's key alone (novox/hq ADR 0260): a bar shows its own machine's draw.
|
||||||
d.Reads = append(append([]string(nil), d.Reads...), granted...)
|
for _, sr := range catalogue.ReadsGranted(m, onMachine(declared, modules), n.Name) {
|
||||||
|
d.KeyedReads = append(d.KeyedReads, broker.KeyedRead{Read: sr.Read, Key: sr.Key})
|
||||||
}
|
}
|
||||||
d.Holds = heldHere(d.Holds, holdings, n.Name, module)
|
d.Holds = heldHere(d.Holds, holdings, n.Name, module)
|
||||||
out.Assigned[n.Name] = append(out.Assigned[n.Name], d)
|
out.Assigned[n.Name] = append(out.Assigned[n.Name], d)
|
||||||
@@ -182,7 +183,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
|||||||
func bucketsOf(m catalogue.Manifest) []broker.Bucket {
|
func bucketsOf(m catalogue.Manifest) []broker.Bucket {
|
||||||
var out []broker.Bucket
|
var out []broker.Bucket
|
||||||
for _, s := range m.State {
|
for _, s := range m.State {
|
||||||
out = append(out, broker.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds})
|
out = append(out, broker.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds,
|
||||||
|
PerMachine: s.PerMachine})
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,16 +322,20 @@ func TestABarIsGrantedTheStateItsMachinesModulesOfferIt(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
reads := func(node, module string) []string {
|
reads := func(node, module string) []broker.KeyedRead {
|
||||||
for _, d := range records.Assigned[node] {
|
for _, d := range records.Assigned[node] {
|
||||||
if d.Module == module {
|
if d.Module == module {
|
||||||
return d.Reads
|
if len(d.Reads) != 0 {
|
||||||
|
t.Fatalf("%s on %s reads whole buckets: %v", module, node, d.Reads)
|
||||||
|
}
|
||||||
|
return d.KeyedReads
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Fatalf("%s is not on %s", module, node)
|
t.Fatalf("%s is not on %s", module, node)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != "power.draw" {
|
// The laptop's own key, and no other (novox/hq ADR 0260).
|
||||||
|
if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != (broker.KeyedRead{Read: "power.draw", Key: "laptop"}) {
|
||||||
t.Fatalf("the laptop's bar reads %v", got)
|
t.Fatalf("the laptop's bar reads %v", got)
|
||||||
}
|
}
|
||||||
if got := reads("desk", "a-bar"); len(got) != 0 {
|
if got := reads("desk", "a-bar"); len(got) != 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user