Files
mesh-catalog/modules/pacman/cmd/pacman-tools/view.go
T
jschoubben 62437f8964 issue 1840/S8: answer the package view from the bus, so every node is read at once and none is asked
pacman_packages answers one package on every node, or each node's counts, its explicit packages no module
declares and its foreign ones apart, the versions that differ and the packages on some nodes only, with a
node over the bus's cap marked not current. pacman_updates, pacman_installed and pacman_foreign read the
keeper's states instead of the machine; pacman_updates says when each node's list last changed.
2026-10-22 14:36:11 +02:00

378 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
// controller's `declared-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"
)
// DeclaredPackagesState is the controller's state of the packages each node's modules declare (ADR 0507 §5),
// read through the manifest's `reads`.
const DeclaredPackagesState = "mesh-controller.declared-packages"
// declaredValue is a node's value of `declared-packages`, as the controller writes it.
type declaredValue struct {
Node string `json:"node"`
Packages []struct {
Package string `json:"package"`
Modules []string `json:"modules"`
Absent bool `json:"absent"`
} `json:"packages"`
}
// 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 the controller holds nothing for 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 controller holds 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
}