Work out a node's declared packages and compose a node once over one read (ADR 0565 §6, ADR 0580; issues 1953, 1954) #155
@@ -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