Files
mesh-catalog/modules/pacman/cmd/pacman-tools/view.go
T

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
}