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 (``), sorted, and the marks (`.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 }