Work out a node's declared packages and their difference with the mirror in the SDK, so each engine writes its own key (ADR 0565 clause 6, issue 1953)
The engine composes its node's wants itself, so the derivation the controller keeps privately moves beside the layout: DeclaredPackagesOf gives the mirror's bytes from the compositions, DiffDeclaredPackages what the node seat's verb packages compares until the lease write, and EnginePackagesRead names the state as readers declare it (mesh-host.packages, as mesh-host.data). Each engine may read the controller mirror's key of its own node alone. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -96,6 +96,9 @@ func engineBeyond(user, g string) bool {
|
||||
strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".") ||
|
||||
// And its node's declared packages (novox/hq ADR 0565 clause 6).
|
||||
strings.Contains(g, "KV_"+EnginePackagesBucket) || g == "$KV."+EnginePackagesBucket+"."+node ||
|
||||
// And, read, the controller's mirror's key of its node, until the lease write (novox/hq ADR 0565 clause 6).
|
||||
strings.Contains(g, "KV_"+DeclaredPackagesBucket) && !strings.HasPrefix(g, "$KV.") &&
|
||||
(!strings.Contains(g, "$KV.") || strings.HasSuffix(g, "$KV."+DeclaredPackagesBucket+"."+node)) ||
|
||||
// And its own health report (issue 774).
|
||||
strings.Contains(g, "KV_"+delivery.NodeHealthBucket) || g == "$KV."+delivery.NodeHealthBucket+"."+node ||
|
||||
// And the provisions of its node's modules, read (novox/hq ADR 0570 clause 6).
|
||||
@@ -815,7 +818,11 @@ func healthChecks(t *testing.T) {
|
||||
MayPublish(perms, "$KV.mesh-host_health.b")) {
|
||||
t.Errorf("the watchman reads the health reports %v, %v", MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.b"), perms.Publish)
|
||||
}
|
||||
if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b")) {
|
||||
// ADR 0565 clause 6: and it reads the controller's mirror's key of its own node alone, and writes none of it.
|
||||
if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b") ||
|
||||
!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-controller_declared-packages.$KV.mesh-controller_declared-packages.a") ||
|
||||
MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-controller_declared-packages.$KV.mesh-controller_declared-packages.b") ||
|
||||
MayPublish(perms, "$KV.mesh-controller_declared-packages.a")) {
|
||||
t.Errorf("node %s writes the packages keys %v", u.Node, perms.Publish)
|
||||
}
|
||||
if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") {
|
||||
|
||||
@@ -74,9 +74,11 @@ func engineGrants(p Principal) (pub, sub []string) {
|
||||
if own, err := NodeKeysGrants(EngineDataBucket, p.Node); err == nil {
|
||||
pub = append(pub, own...)
|
||||
}
|
||||
// And its node's declared packages, its own key alone (ADR 0565 clause 6).
|
||||
// And its node's declared packages, its own key alone (ADR 0565 clause 6); and, read, the controller's mirror's key
|
||||
// of its node, which the node seat's verb packages compares it with until the lease write ends the mirror.
|
||||
if safeSubject.MatchString(p.Node) {
|
||||
pub = append(pub, EnginePackagesGrants(p.Node)...)
|
||||
pub = append(pub, readGrants(DeclaredPackagesBucket, p.Node)...)
|
||||
}
|
||||
return append(pub, tp...), append(sub, ts...)
|
||||
}
|
||||
|
||||
@@ -288,7 +288,8 @@ var WritersTable = []WriterRow{
|
||||
KeptIn: "key-value " + PredefinedBucket, Others: "read through `reads` as mesh-controller." + MirrorPredefined,
|
||||
Subjects: kvOf(PredefinedBucket), Writes: isController},
|
||||
{State: "the declared-packages mirror", Writer: "controller (lease holder), from the composition of each node, only on a change",
|
||||
KeptIn: "key-value " + DeclaredPackagesBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredPackages,
|
||||
KeptIn: "key-value " + DeclaredPackagesBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredPackages +
|
||||
", and each node's engine its node's key, until the lease write (ADR 0565 clause 6)",
|
||||
Subjects: kvOf(DeclaredPackagesBucket), Writes: isController},
|
||||
// What a machine's provisioner client placed of a module's files (novox/hq ADR 0349 §4, design 57 P6): its own
|
||||
// machine's word alone, which a walk's end and a gate wait for (ADR 0362, ADR 0349 §15). Kept newest per module
|
||||
|
||||
@@ -142,6 +142,40 @@ func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
|
||||
t.Errorf("%s keyed %q, %v", pull, got, err)
|
||||
}
|
||||
}
|
||||
// ADR 0565 §6: the engine works out its node's key of `packages` from the compositions it composed, the mirror's bytes
|
||||
// for the same resources (mesh-controller internal/inventory/declared_packages_test.go aNodesPackages at 0fbb5ef3):
|
||||
// a module's own composition, the node's own with a folded module's resource named by its id alone, and a file.
|
||||
res := func(s ...string) []json.RawMessage {
|
||||
var out []json.RawMessage
|
||||
for _, r := range s {
|
||||
out = append(out, json.RawMessage(r))
|
||||
}
|
||||
return out
|
||||
}
|
||||
comps := []Composed{
|
||||
{Node: "one", Module: "web", Resources: res(`{"id":"pkg-curl","type":"package","package":"curl"}`,
|
||||
`{"id":"pkg-nano","type":"package","package":"nano","absent":true}`, `{"id":"conf","type":"file","path":"/etc/web.conf"}`)},
|
||||
{Node: "one", Resources: res(`{"id":"backup.pkg-curl","type":"package","package":"curl"}`)},
|
||||
}
|
||||
want := `{"node":"one","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
|
||||
`{"package":"nano","modules":["web"],"absent":true}]}`
|
||||
got, err := DeclaredPackagesOf("one", comps)
|
||||
if raw, _ := json.Marshal(got); err != nil || string(raw) != want {
|
||||
t.Errorf("the node's packages are %s (%v)", raw, err)
|
||||
}
|
||||
if none, _ := DeclaredPackagesOf("one", nil); string(jsonOf(none)) != `{"node":"one","packages":[]}` || EnginePackagesRead != "mesh-host.packages" {
|
||||
t.Errorf("a node declaring no package is %s", jsonOf(none))
|
||||
}
|
||||
// Their difference with the mirror: a package only one side holds, and one whose modules differ; none when they agree.
|
||||
mirror := DeclaredPackages{Node: "one", Packages: []PackageDeclared{{Package: "curl", Modules: []string{"web"}},
|
||||
{Package: "nano", Modules: []string{"web"}, Absent: true}, {Package: "vim", Modules: []string{"edit"}}}}
|
||||
if d := DiffDeclaredPackages(got, mirror); len(d) != 2 || d[0].Package != "curl" || strings.Join(d[0].Engine, ",") != "backup,web" ||
|
||||
d[1].Package != "vim" || len(d[1].Engine) != 0 || d[1].Mirror[0] != "edit" {
|
||||
t.Errorf("the difference is %+v", d)
|
||||
}
|
||||
if d := DiffDeclaredPackages(got, got); d == nil || len(d) != 0 {
|
||||
t.Errorf("one declaration differs from itself: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonOf(v any) []byte {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
)
|
||||
|
||||
// **A node's declared packages, worked out by its engine** (novox/hq ADR 0565 §1, §6; issue 1953): the derivation and
|
||||
// the comparison of the node-engine's state `packages` (EnginePackagesState, layout DeclaredPackages), so the engine
|
||||
// that writes the key, the node seat's verb that compares it with the controller's mirror, and any reader work them out
|
||||
// one way. The controller's own copy of the derivation (mesh-controller internal/inventory DeclaredPackagesValue) is
|
||||
// not changed (ADR 0547 §1) and ends with its lease.
|
||||
|
||||
// EnginePackagesRead is the engine's state `packages` as a reader names it in `reads`: the node-engine's manifest is
|
||||
// mesh-host's, so its bucket is mesh-host_packages, read as disk-load reads mesh-host.data (EngineDataRead).
|
||||
const EnginePackagesRead = "mesh-host." + EnginePackagesState
|
||||
|
||||
// DeclaredPackagesOf is a node's key of `packages` from the compositions its engine composed for the node's wants:
|
||||
// each resource of type package, by its name and whether it is declared absent, with the modules that declare it so,
|
||||
// in the mirror's layout and order (names sorted, absent after present, modules sorted). A resource's module is the
|
||||
// one it names, else its composition's, else the guess from its id (declaration.ModuleGuess); a package of no module
|
||||
// lists none.
|
||||
func DeclaredPackagesOf(node string, compositions []Composed) (DeclaredPackages, error) {
|
||||
by := map[packageKey]map[string]bool{}
|
||||
for _, c := range compositions {
|
||||
for _, raw := range c.Resources {
|
||||
var r struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Package string `json:"package"`
|
||||
Absent bool `json:"absent"`
|
||||
Module string `json:"module"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return DeclaredPackages{}, fmt.Errorf("delivery: a resource of %s's composition %q is not readable: %w", node, c.Module, err)
|
||||
}
|
||||
if r.Type != "package" || r.Package == "" {
|
||||
continue
|
||||
}
|
||||
k := packageKey{r.Package, r.Absent}
|
||||
if by[k] == nil {
|
||||
by[k] = map[string]bool{}
|
||||
}
|
||||
module := r.Module
|
||||
if module == "" {
|
||||
module = c.Module
|
||||
}
|
||||
if module == "" {
|
||||
module, _ = declaration.ModuleGuess(r.ID)
|
||||
}
|
||||
if module != "" {
|
||||
by[k][module] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
v := DeclaredPackages{Node: node, Packages: []PackageDeclared{}}
|
||||
for _, k := range sortedPackageKeys(by) {
|
||||
modules := make([]string, 0, len(by[k]))
|
||||
for m := range by[k] {
|
||||
modules = append(modules, m)
|
||||
}
|
||||
sort.Strings(modules)
|
||||
v.Packages = append(v.Packages, PackageDeclared{Package: k.name, Modules: modules, Absent: k.absent})
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// PackageDifference is one package two declarations of a node hold differently: the modules each side declares it
|
||||
// with (present, or absent when Absent), none on a side that does not hold it.
|
||||
type PackageDifference struct {
|
||||
Package string `json:"package"`
|
||||
Absent bool `json:"absent"`
|
||||
Engine []string `json:"engine"`
|
||||
Mirror []string `json:"mirror"`
|
||||
}
|
||||
|
||||
// DiffDeclaredPackages is every package the engine's key and the controller's mirror hold differently for one node, in
|
||||
// the layout's order: what the node seat's verb packages answers until the lease write (ADR 0565 §6). Empty when they
|
||||
// agree.
|
||||
func DiffDeclaredPackages(engine, mirror DeclaredPackages) []PackageDifference {
|
||||
index := func(d DeclaredPackages) map[packageKey][]string {
|
||||
out := map[packageKey][]string{}
|
||||
for _, p := range d.Packages {
|
||||
out[packageKey{p.Package, p.Absent}] = append([]string{}, p.Modules...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
e, m := index(engine), index(mirror)
|
||||
union := map[packageKey]bool{}
|
||||
for k := range e {
|
||||
union[k] = true
|
||||
}
|
||||
for k := range m {
|
||||
union[k] = true
|
||||
}
|
||||
out := []PackageDifference{}
|
||||
for _, k := range sortedPackageKeys(union) {
|
||||
em, inE := e[k]
|
||||
mm, inM := m[k]
|
||||
if inE && inM && slices.Equal(em, mm) {
|
||||
continue
|
||||
}
|
||||
if em == nil {
|
||||
em = []string{}
|
||||
}
|
||||
if mm == nil {
|
||||
mm = []string{}
|
||||
}
|
||||
out = append(out, PackageDifference{Package: k.name, Absent: k.absent, Engine: em, Mirror: mm})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// packageKey is a package as the layout tells it apart: its name, and whether it is declared absent.
|
||||
type packageKey struct {
|
||||
name string
|
||||
absent bool
|
||||
}
|
||||
|
||||
// sortedPackageKeys is the keys of m by name, absent after present.
|
||||
func sortedPackageKeys[V any](m map[packageKey]V) []packageKey {
|
||||
keys := make([]packageKey, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].name != keys[j].name {
|
||||
return keys[i].name < keys[j].name
|
||||
}
|
||||
return !keys[i].absent && keys[j].absent
|
||||
})
|
||||
return keys
|
||||
}
|
||||
Reference in New Issue
Block a user