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 }