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
138 lines
4.6 KiB
Go
138 lines
4.6 KiB
Go
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
|
|
}
|