Files
mesh-sdk/go/catalogue/seat_dependencies.go
T

154 lines
5.9 KiB
Go

package catalogue
import (
"fmt"
"sort"
"strings"
)
// The three seats that apply resources (novox/hq ADR 0207 §1). Named once, because the derivation,
// the seed and the messages all turn on these strings.
const (
ServiceManagerSeat = "node-service-manager"
PackageManagerSeat = "node-package-manager"
ContainerRuntimeSeat = "node-container-runtime"
)
// appliedThrough is which seat applies a resource of each type. **Only these three**: ADR 0207
// names them and no more. A process is supervised by the host itself, a file, a directory, an
// archive, a user or an action is the host's own act, and a module's other kinds reach the machine
// without a role in between — adding one here is a decision, not a refinement.
var appliedThrough = map[string]string{
"service": ServiceManagerSeat,
"package": PackageManagerSeat,
"container": ContainerRuntimeSeat,
}
// isFoundation is whether a module's declarations are the foundation's rather than its own: a module whose resources are
// computed (`computed`, the private network's peer list written per node) is the mesh's by construction. Read from the
// manifest alone, never from its name (issue 774, the operator 2026-11-03): the node-engine's own manifest declares no
// resource a seat applies, so it depends on none by the rules below.
func isFoundation(m Manifest) bool {
return m.Computed != ""
}
// DependsOn is every seat a module needs held on its node: derived from the resource types it
// declares itself (novox/hq ADR 0207 §2), from what it contributes to (ADR 0210), and from what a
// node seat it claims needs beside it — read from whichever manifest in `catalogue` declares that
// seat (novox/hq ADR 0220, ADR 0378 §8). Sorted.
//
// **The module's own `resources` only**, for the first source. What the controller composes around
// a module — its filter, jails, certificates, kept files, bundles, the guard — is the mesh's, put
// there because the module is assigned, and depending on it would make the module answer for the
// mesh's choices.
func DependsOn(catalogue map[string]Manifest, m Manifest) []string {
if isFoundation(m) {
return nil
}
seen := map[string]bool{}
for _, r := range m.Resources {
if seat, applied := appliedThrough[fmt.Sprint(r["type"])]; applied {
seen[seat] = true
}
}
for _, seat := range contributedTo(m) {
seen[seat] = true
}
for _, seat := range neededBesideClaims(catalogue, m) {
seen[seat] = true
}
out := make([]string, 0, len(seen))
for s := range seen {
out = append(out, s)
}
sort.Strings(out)
return out
}
// contributedTo is every seat a module contributes to (novox/hq ADR 0210 §3): a contribution is
// configuration only the seat's holder applies, so it is a dependency on that seat exactly as a
// resource is on the seat that applies it. The environment goes to node-environment's holder
// (ADR 0203); shell code to the holder that places it for its target — the login shell's for a
// shell, the display server's for the session's start and resources (ADR 0204, ADR 0208 §4).
func contributedTo(m Manifest) []string {
var out []string
if e := m.Environment; e != nil && (len(e.Variables) > 0 || len(e.Path) > 0) {
out = append(out, EnvironmentSeat)
}
for _, c := range m.Shell {
out = append(out, placerOf(c.For))
}
// Any other seat's contribution (novox/hq ADR 0212 §4), by the seat's canonical name; one the
// mesh does not define is refused at registration and depends on nothing here.
// A kind the seat only offers to place makes no dependency (novox/hq ADR 0255).
for _, c := range m.Contributions {
if s, r, ok := receivable(c.Seat, c.Kind); ok && r.Offered {
continue
} else if s.Name != "" {
out = append(out, s.Name)
}
}
// And the backup holder for what is irreplaceable (novox/hq ADR 0233): it backs it up or watches the
// array it is on. What is valuable is backed up where a holder is — the standard plan — and makes
// no machine refuse it for want of one.
if m.NeedsBackupHolder() {
out = append(out, BackupSeat)
}
return out
}
// neededBesideClaims is every seat a node seat the module claims needs held on the same node
// (novox/hq ADR 0220, ADR 0378 §8): a module-declared seat's needs are read from whichever manifest
// in the catalogue declares that seat, never written again in the claimant's own manifest. Derived
// from the claim, as a resource's seat is derived from its type, so a module that claims the seat
// cannot leave the dependency out. A compiled seat carries no needs (novox/hq ADR 0378 §8).
func neededBesideClaims(catalogue map[string]Manifest, m Manifest) []string {
var out []string
for _, seat := range nodeSeatsClaimed(m) {
for _, candidate := range catalogue {
for _, s := range candidate.DefinesSeats {
if s.Name == seat {
out = append(out, s.Needs...)
}
}
}
}
return out
}
// Unheld is one dependency of one module on a node that nothing on that node holds.
type Unheld struct {
Node string `json:"node"`
Module string `json:"module"`
Seat string `json:"seat"`
// Holders are the modules in the catalogue that could hold the seat: assigning one meets it.
Holders []string `json:"holders"`
}
// String is the line a refusal and a report both say, so the two never drift.
func (u Unheld) String() string {
remedy := "and no module in the catalogue claims it yet"
if len(u.Holders) > 0 {
remedy = "— assign one that holds it: " + strings.Join(u.Holders, ", ")
}
return fmt.Sprintf("%s on %s depends on %s, which nothing on %s holds (novox/hq ADR 0207) %s",
u.Module, u.Node, u.Seat, u.Node, remedy)
}
// nodeSeatsClaimed is every node seat a module claims, each by its current name (ADR 0122), so
// a dependency on any of them — a resource's or a contribution's — is met by the claim.
func nodeSeatsClaimed(m Manifest) []string {
var out []string
for _, c := range m.Claims {
if c.At() != ScopeNode {
continue
}
name := c.Name
if s, known := SeatNamed(name); known {
name = s.Name
}
out = append(out, name)
}
return out
}