374 lines
11 KiB
Go
374 lines
11 KiB
Go
package main
|
|
|
|
// The package view (novox/hq ADR 0507 §6, §7; acceptance 1840/AC6): verbs that read the module's states and the
|
|
// node-engine's `packages`, so any instance answers for every node and no machine is asked. A node that is
|
|
// off keeps its last values, shown with the time they describe; a node whose packages are over the bus's cap is
|
|
// marked not current. A foreign package is listed apart in every answer.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// DeclaredPackagesState is the node-engine's state of the packages its node's modules declare, one key per node,
|
|
// written by that node's engine alone (ADR 0565 clause 6, which moved ADR 0507 §5's mirror off the controller), read
|
|
// through the manifest's `reads`.
|
|
const DeclaredPackagesState = "node-engine." + delivery.EnginePackagesState
|
|
|
|
// declaredValue is a node's key of `packages`, in mesh-sdk's layout (the controller's mirror unchanged).
|
|
type declaredValue = delivery.DeclaredPackages
|
|
|
|
// nodeInstalled is one node's key of `installed` with its not-current mark.
|
|
type nodeInstalled struct {
|
|
Node string
|
|
Value InstalledValue
|
|
NotCurrent *NotCurrent
|
|
}
|
|
|
|
func readValue(r StateReader, key string, v any) (bool, error) {
|
|
e, err := r.Get(key)
|
|
if err != nil || e == nil {
|
|
return false, err
|
|
}
|
|
if err := json.Unmarshal(e.Value, v); err != nil {
|
|
return false, fmt.Errorf("%s is not readable: %w", key, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// nodeKeys are the node keys of a state (`<node>`), sorted, and the marks (`<node>.not-current`) apart.
|
|
func nodeKeys(r StateReader) (nodes []string, marks map[string]bool, err error) {
|
|
keys, err := r.Keys()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
marks = map[string]bool{}
|
|
for _, k := range keys {
|
|
if node, ok := strings.CutSuffix(k, ".not-current"); ok {
|
|
marks[node] = true
|
|
continue
|
|
}
|
|
if !strings.Contains(k, ".") {
|
|
nodes = append(nodes, k)
|
|
}
|
|
}
|
|
sort.Strings(nodes)
|
|
return nodes, marks, nil
|
|
}
|
|
|
|
func (m *Machine) states() error {
|
|
if m.States == nil {
|
|
return fmt.Errorf("the module's states cannot be read here")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// installedOn is the installed packages of one node, or of every node when node is empty.
|
|
func (m *Machine) installedOn(node string) ([]nodeInstalled, error) {
|
|
if err := m.states(); err != nil {
|
|
return nil, err
|
|
}
|
|
r := m.States(StateInstalled)
|
|
nodes, marks, err := nodeKeys(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if node != "" {
|
|
nodes = []string{node}
|
|
}
|
|
var out []nodeInstalled
|
|
for _, n := range nodes {
|
|
ni := nodeInstalled{Node: n}
|
|
found, err := readValue(r, n, &ni.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
if node != "" {
|
|
return nil, fmt.Errorf("no packages of %s are on the bus: its keeper has not written them", node)
|
|
}
|
|
continue
|
|
}
|
|
if marks[n] || node != "" {
|
|
var nc NotCurrent
|
|
if ok, err := readValue(r, NotCurrentKey(n), &nc); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
ni.NotCurrent = &nc
|
|
}
|
|
}
|
|
out = append(out, ni)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// declaredOn is what each node's modules declare present, by node and package, with the declaring modules; a
|
|
// node whose engine has written no key is missing.
|
|
func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, error) {
|
|
r := m.States(DeclaredPackagesState)
|
|
out := map[string]map[string][]string{}
|
|
for _, n := range nodes {
|
|
var v declaredValue
|
|
found, err := readValue(r, n, &v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
out[n] = map[string][]string{}
|
|
for _, p := range v.Packages {
|
|
if !p.Absent {
|
|
out[n][p.Package] = p.Modules
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// NodeView is one node in the view.
|
|
type NodeView struct {
|
|
Node string `json:"node"`
|
|
// At is the time of the transaction that left the node's packages as shown.
|
|
At time.Time `json:"at"`
|
|
Packages int `json:"packages"`
|
|
Explicit int `json:"explicit"`
|
|
// Current is false while the node's packages are over the bus's cap: what is shown is older.
|
|
Current bool `json:"current"`
|
|
NotCurrent *NotCurrent `json:"not_current,omitempty"`
|
|
// Undeclared is the packages installed explicitly from a repository that no module assigned to the node
|
|
// declares; null when the node-engine has published no declaration of the node.
|
|
Undeclared []PackageVersion `json:"undeclared"`
|
|
Foreign []PackageVersion `json:"foreign"`
|
|
}
|
|
|
|
// Spread is one package across the nodes: its version on each node that has it.
|
|
type Spread struct {
|
|
Name string `json:"name"`
|
|
Versions map[string]string `json:"versions"`
|
|
}
|
|
|
|
// PackageOnNode is one package on one node.
|
|
type PackageOnNode struct {
|
|
Node string `json:"node"`
|
|
Installed bool `json:"installed"`
|
|
Version string `json:"version,omitempty"`
|
|
Explicit bool `json:"explicit,omitempty"`
|
|
Repository string `json:"repository,omitempty"`
|
|
Foreign bool `json:"foreign,omitempty"`
|
|
DeclaredBy []string `json:"declared_by"`
|
|
Current bool `json:"current"`
|
|
}
|
|
|
|
// Packages is pacman_packages: one package on every node, or the whole view: per node its counts, its explicit
|
|
// undeclared packages and its foreign ones; the packages whose versions differ between nodes, and the packages
|
|
// on some nodes only, among the packages from a repository (each node's foreign ones are in its own list).
|
|
func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
|
|
all, err := m.installedOn("")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes := make([]string, 0, len(all))
|
|
for _, ni := range all {
|
|
nodes = append(nodes, ni.Node)
|
|
}
|
|
declared, err := m.declaredOn(nodes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if name != "" {
|
|
on := []PackageOnNode{}
|
|
for _, ni := range all {
|
|
p := PackageOnNode{Node: ni.Node, Current: ni.NotCurrent == nil, DeclaredBy: declared[ni.Node][name]}
|
|
if p.DeclaredBy == nil {
|
|
p.DeclaredBy = []string{}
|
|
}
|
|
for _, ip := range ni.Value.Packages {
|
|
if ip.Name == name {
|
|
p.Installed, p.Version, p.Explicit = true, ip.Version, ip.Explicit
|
|
p.Foreign = ip.Repo == RepoForeign
|
|
if !p.Foreign {
|
|
p.Repository = ip.Repo
|
|
}
|
|
}
|
|
}
|
|
on = append(on, p)
|
|
}
|
|
return map[string]any{"package": name, "nodes": on}, nil
|
|
}
|
|
views := []NodeView{}
|
|
versions := map[string]map[string]string{}
|
|
for _, ni := range all {
|
|
v := NodeView{Node: ni.Node, At: ni.Value.At, Packages: len(ni.Value.Packages), Current: ni.NotCurrent == nil,
|
|
NotCurrent: ni.NotCurrent, Foreign: []PackageVersion{}}
|
|
d, known := declared[ni.Node]
|
|
if known {
|
|
v.Undeclared = []PackageVersion{}
|
|
}
|
|
for _, p := range ni.Value.Packages {
|
|
if p.Explicit {
|
|
v.Explicit++
|
|
}
|
|
if p.Repo == RepoForeign {
|
|
v.Foreign = append(v.Foreign, PackageVersion{p.Name, p.Version})
|
|
continue
|
|
}
|
|
if versions[p.Name] == nil {
|
|
versions[p.Name] = map[string]string{}
|
|
}
|
|
versions[p.Name][ni.Node] = p.Version
|
|
if known && p.Explicit && d[p.Name] == nil {
|
|
v.Undeclared = append(v.Undeclared, PackageVersion{p.Name, p.Version})
|
|
}
|
|
}
|
|
views = append(views, v)
|
|
}
|
|
names := make([]string, 0, len(versions))
|
|
for n := range versions {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
differing, some := []Spread{}, []Spread{}
|
|
for _, n := range names {
|
|
vs := versions[n]
|
|
if len(vs) < len(all) {
|
|
some = append(some, Spread{n, vs})
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, ver := range vs {
|
|
seen[ver] = true
|
|
}
|
|
if len(seen) > 1 {
|
|
differing = append(differing, Spread{n, vs})
|
|
}
|
|
}
|
|
out := map[string]any{"nodes": views}
|
|
for key, list := range map[string][]Spread{"differing": differing, "some_nodes": some} {
|
|
b := bound(key, list, limit)
|
|
out[key] = b[key]
|
|
out[key+"_count"] = b["count"]
|
|
out[key+"_truncated"] = b["truncated"]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// UpdatesView is one node's `updates` with the time its key last changed.
|
|
type UpdatesView struct {
|
|
Node string `json:"node"`
|
|
Reachable bool `json:"reachable"`
|
|
Count int `json:"count"`
|
|
Packages []VersionChange `json:"packages"`
|
|
Changed *time.Time `json:"changed,omitempty"`
|
|
}
|
|
|
|
// UpdatesFor is pacman_updates: what a full upgrade would change, on one node or on every node, each with when its
|
|
// key last changed (the bus's time of the write; the value holds no time of a read, ADR 0289 §4).
|
|
func (m *Machine) UpdatesFor(node string) (map[string]any, error) {
|
|
if err := m.states(); err != nil {
|
|
return nil, err
|
|
}
|
|
r := m.States(StateUpdates)
|
|
nodes := []string{node}
|
|
if node == "" {
|
|
var err error
|
|
if nodes, _, err = nodeKeys(r); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
views := []UpdatesView{}
|
|
for _, n := range nodes {
|
|
var v UpdatesValue
|
|
found, err := readValue(r, n, &v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
if node != "" {
|
|
return nil, fmt.Errorf("no updates of %s are on the bus: its keeper has not written them", node)
|
|
}
|
|
continue
|
|
}
|
|
uv := UpdatesView{Node: n, Reachable: v.Reachable, Count: len(v.Packages), Packages: v.Packages}
|
|
if uv.Packages == nil {
|
|
uv.Packages = []VersionChange{}
|
|
}
|
|
if h, err := r.History(n); err == nil && len(h) > 0 {
|
|
at := h[len(h)-1].At
|
|
uv.Changed = &at
|
|
}
|
|
views = append(views, uv)
|
|
}
|
|
return map[string]any{"nodes": views}, nil
|
|
}
|
|
|
|
// InstalledFrom is pacman_installed from the bus: a node's installed packages with version, reason, repository
|
|
// and whether foreign, narrowed by name, reason or foreign, with totals, the time they describe and whether
|
|
// they are current.
|
|
func (m *Machine) InstalledFrom(node, match, reason string, foreignOnly bool, limit int) (map[string]any, error) {
|
|
ni, err := m.oneNode(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgs := []Package{}
|
|
counts := map[string]int{"explicit": 0, "dependency": 0, "foreign": 0}
|
|
for _, ip := range ni.Value.Packages {
|
|
p := Package{Name: ip.Name, Version: ip.Version, Reason: "dependency", Foreign: ip.Repo == RepoForeign}
|
|
if ip.Explicit {
|
|
p.Reason = "explicit"
|
|
}
|
|
if !p.Foreign {
|
|
p.Repository = ip.Repo
|
|
}
|
|
counts[p.Reason]++
|
|
if p.Foreign {
|
|
counts["foreign"]++
|
|
}
|
|
if match != "" && !strings.Contains(p.Name, match) || reason != "" && p.Reason != reason || foreignOnly && !p.Foreign {
|
|
continue
|
|
}
|
|
pkgs = append(pkgs, p)
|
|
}
|
|
out := bound("packages", pkgs, limit)
|
|
out["totals"] = counts
|
|
out["node"], out["at"], out["current"] = ni.Node, ni.Value.At, ni.NotCurrent == nil
|
|
if ni.NotCurrent != nil {
|
|
out["not_current"] = ni.NotCurrent
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ForeignFrom is pacman_foreign from the bus: a node's packages from outside the repositories.
|
|
func (m *Machine) ForeignFrom(node string) (map[string]any, error) {
|
|
ni, err := m.oneNode(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgs := []PackageVersion{}
|
|
for _, ip := range ni.Value.Packages {
|
|
if ip.Repo == RepoForeign {
|
|
pkgs = append(pkgs, PackageVersion{ip.Name, ip.Version})
|
|
}
|
|
}
|
|
return map[string]any{"node": ni.Node, "count": len(pkgs), "packages": pkgs, "at": ni.Value.At, "current": ni.NotCurrent == nil}, nil
|
|
}
|
|
|
|
// oneNode is the named node's packages, this node's when none is named.
|
|
func (m *Machine) oneNode(node string) (nodeInstalled, error) {
|
|
if node == "" {
|
|
node = m.Node
|
|
}
|
|
if node == "" {
|
|
return nodeInstalled{}, fmt.Errorf("node is required: this bundle does not know its own")
|
|
}
|
|
all, err := m.installedOn(node)
|
|
if err != nil {
|
|
return nodeInstalled{}, err
|
|
}
|
|
return all[0], nil
|
|
}
|