package main // The keeper's bus shapes (novox/hq ADR 0507, ADR 0508): the value of every key of the module's states and the // body of every event its process emits, in one place, so the process that writes them and the verbs that read // them agree by type. Each is a struct of named fields only: no map and no free-form value, so nothing but what // is named here goes on the bus (ADR 0507 §1: package names, versions, unit and container names, file paths and // exit statuses, never a process's command line or environment). A time in a value is the time of the thing it // describes (a transaction, a step, an opening), never the time it was read: a key is written only when what it // describes changed (ADR 0289 §1), and no field changes on a read alone (ADR 0289 §4). When a key last changed // is the bus's to say (its revision), not a field's. // // A key is `` or `.`: a node name and an id are each one key token ([A-Za-z0-9_-]), so the // dot only ever separates them. import ( "fmt" "time" ) // The module's states (manifest `state` and the keeper's `bus` `state`). const ( // StateInstalled holds each node's installed packages: key `` an InstalledValue, and key // `.not-current` a NotCurrent while the node's packages are over the bus's cap (ADR 0507 §2). StateInstalled = "installed" // StateUpdates holds what a full upgrade would change: key `` an UpdatesValue (ADR 0507 §4). StateUpdates = "updates" // StateRuns holds every upgrade and rollback: key `.` a Run, kept (ADR 0508 §1). StateRuns = "runs" // StateRecords holds the package record taken before a run: key `.` a Record, deleted after // the config value `record-days` (ADR 0508 §2). StateRecords = "records" // StateRounds holds the weekly rounds: key `` a Round, created once by the first node of the order, // and key `.` that node's RoundNode (ADR 0508 §5, §6). StateRounds = "rounds" // StateAsks holds the acts asked of a node: key `.` an Ask, taken by that node's keeper alone // (ADR 0508 §8). StateAsks = "asks" ) // States are the module's states, in the manifest's order. var States = []string{StateInstalled, StateUpdates, StateRuns, StateRecords, StateRounds, StateAsks} // The events the keeper emits (the keeper's `bus` `emits`, and the manifest's `emits`). const ( EventPackageChanged = "package.changed" // PackageChanged (ADR 0507 §3) EventRecorded = "upgrade.recorded" // UpgradeRecorded (ADR 0508 §1, §2) EventKernelHeld = "upgrade.kernel-held" // KernelHeld (ADR 0508 §3) EventStarted = "upgrade.started" // UpgradeStarted EventFinished = "upgrade.finished" // UpgradeFinished EventFailed = "upgrade.failed" // UpgradeFailed EventChecked = "upgrade.checked" // UpgradeChecked (ADR 0508 §4) EventRollbackStarted = "rollback.started" // RollbackStarted (ADR 0508 §7) EventRollbackFinished = "rollback.finished" // RollbackFinished ) // Events are the events the keeper emits, each with the type of its body. var Events = []struct { Name string Body any }{ {EventPackageChanged, PackageChanged{}}, {EventRecorded, UpgradeRecorded{}}, {EventKernelHeld, KernelHeld{}}, {EventStarted, UpgradeStarted{}}, {EventFinished, UpgradeFinished{}}, {EventFailed, UpgradeFailed{}}, {EventChecked, UpgradeChecked{}}, {EventRollbackStarted, RollbackStarted{}}, {EventRollbackFinished, RollbackFinished{}}, } // Key names. // NotCurrentKey is the key in `installed` that marks a node's packages as not current. func NotCurrentKey(node string) string { return node + ".not-current" } // NodeKey is a key `.`: a run in `runs` or `records`, an ask in `asks`. func NodeKey(node, id string) string { return node + "." + id } // RoundNodeKey is a node's key in a round: `.` in `rounds`. func RoundNodeKey(round, node string) string { return round + "." + node } // RunID names a run by the moment it began, in UTC: `20261024T040012Z`. A run read from the package manager's // log is named by the log's time of its start the same way. func RunID(began time.Time) string { return began.UTC().Format("20060102T150405Z") } // RoundID names the round of the ISO week that holds the given moment, in its own time zone: `2026-W43`. The // round opens on Saturday 04:00 local time, so the local date decides the week. func RoundID(at time.Time) string { year, week := at.ISOWeek() return fmt.Sprintf("%04d-W%02d", year, week) } // installed // InstalledValue is `installed.`: every installed package, sorted by name, and the time of the transaction // that left the database so (ADR 0507 §2). Written compactly: one short entry per package. type InstalledValue struct { At time.Time `json:"at"` Packages []InstalledPackage `json:"packages"` } // InstalledPackage is one installed package: its version, whether it was installed explicitly (else as a // dependency), and its repository, or RepoForeign when no repository holds it (ADR 0507 §7). type InstalledPackage struct { Name string `json:"name"` Version string `json:"version"` Explicit bool `json:"explicit"` Repo string `json:"repo"` } // RepoForeign marks a package from outside the repositories. const RepoForeign = "foreign" // NotCurrent is `installed..not-current`: the node's packages came to more than the bus's cap on a value, // so `installed.` keeps the last value written. Written once, when it first happens, and deleted at the // next successful write of `installed.`. type NotCurrent struct { Bytes int `json:"bytes"` Since time.Time `json:"since"` } // updates // UpdatesValue is `updates.`: every package a full upgrade would change, sorted by name, and whether the // node reached its mirrors (ADR 0507 §4). A failed read writes Reachable false and keeps the last list, never an // empty list in its place. type UpdatesValue struct { Reachable bool `json:"reachable"` Packages []VersionChange `json:"packages"` } // VersionChange is one package going from one version to another. type VersionChange struct { Name string `json:"name"` From string `json:"from"` To string `json:"to"` } // PackageVersion is one package at one version. type PackageVersion struct { Name string `json:"name"` Version string `json:"version"` } // runs // The kinds of run. const ( RunUpgrade = "upgrade" RunRollback = "rollback" ) // What started a run. const ( FromRound = "round" // its turn in the week's round FromAsk = "ask" // an ask: pacman_upgrade, or pacman_rollback with apply FromLog = "log" // read from the package manager's log at the keeper's first start (ADR 0508 §1) ) // The steps of a run, in the order they are written. A run that does not start ends at StepRefused; a // transaction ends at StepFinished or StepFailed, and the after-upgrade check follows either. const ( StepRecorded = "recorded" // the record is written and read back (upgrade only) StepCheckedBefore = "checked-before" // the DKMS gate ran; Held names what it held StepRefused = "refused" // the run did not start; Why says why StepStarted = "started" // the transaction's unit started StepFinished = "finished" // the transaction exited 0 StepFailed = "failed" // the transaction failed StepCheckedAfter = "checked-after" // the after-upgrade check ) // Run is `runs..`: one upgrade or rollback of a node, written at each step and at no other time // (ADR 0508 §1). Round names the round it ran in; Ask the ask that started it (the keeper deletes the ask once // the run names it); Of, for a rollback, the run whose record it returns to. type Run struct { Node string `json:"node"` Run string `json:"run"` Kind string `json:"kind"` From string `json:"from"` Round string `json:"round,omitempty"` Ask string `json:"ask,omitempty"` Of string `json:"of,omitempty"` Steps []Step `json:"steps"` } // Step is one step of a run with its time and outcome; only the fields of its kind are set. type Step struct { Step string `json:"step"` At time.Time `json:"at"` // StepRefused: why the run did not start. Why string `json:"why,omitempty"` // StepCheckedBefore: the kernels held from this run. Held []HeldKernel `json:"held,omitempty"` // StepStarted: the transaction's unit, the packages left out of it (held kernels and their headers), and // for a rollback its plan. Unit string `json:"unit,omitempty"` Ignored []string `json:"ignored,omitempty"` Plan []RollbackItem `json:"plan,omitempty"` // StepFinished and StepFailed: the exit status, the last error line, and what the transaction changed, // read from its lines in the package manager's log. Exit *int `json:"exit,omitempty"` Error string `json:"error,omitempty"` Changes *Changes `json:"changes,omitempty"` // StepCheckedAfter: the after-upgrade check. Check *Check `json:"check,omitempty"` } // HeldKernel is a kernel package left out of a run because an installed DKMS module's own bound excludes its // new version (ADR 0508 §3): `Linux-Maximum` in the module's META, or `BUILD_EXCLUSIVE_KERNEL` in its dkms.conf. type HeldKernel struct { Package string `json:"package"` Version string `json:"version"` Module string `json:"module"` Bound string `json:"bound"` } // Changes is what a transaction changed, each list sorted by name. type Changes struct { Added []PackageVersion `json:"added"` Removed []PackageVersion `json:"removed"` Changed []VersionChange `json:"changed"` } // Check is the after-upgrade check (ADR 0508 §4). Passed when the transaction succeeded, no unit newly failed // and every DKMS module is built for every installed kernel; the rest is said and does not stop the round. type Check struct { Passed bool `json:"passed"` // The .pacnew and .pacsave files the transaction left, by path. Pacnew []string `json:"pacnew"` Pacsave []string `json:"pacsave"` // The distribution's news since the node's previous upgrade. News []NewsTitle `json:"news"` // Why the news could not be read, when it could not: no news is then not said to be none. NewsError string `json:"news_error,omitempty"` // The units and containers whose processes still map a file the upgrade replaced. Mapped []Mapped `json:"mapped"` // Why the mapped files could not be read, when they could not. MappedError string `json:"mapped_error,omitempty"` // The units failed now that were not failed in the record. NewlyFailed []string `json:"newly_failed"` // Whether a restart is needed: the running kernel is not the installed version of its package. RestartNeeded bool `json:"restart_needed"` RunningKernel string `json:"running_kernel"` InstalledKernel string `json:"installed_kernel"` // The DKMS modules not built for an installed kernel. Unbuilt []DKMSBuild `json:"unbuilt"` // Why the DKMS builds could not be read, when they could not; the check then does not pass. DKMSError string `json:"dkms_error,omitempty"` } // NewsTitle is one item of the distribution's news, without its text. type NewsTitle struct { Title string `json:"title"` Link string `json:"link"` Published string `json:"published"` } // Mapped is a process still mapping a replaced file, named by its unit or its container and its executable's // path; never its command line or environment. type Mapped struct { Unit string `json:"unit,omitempty"` Container string `json:"container,omitempty"` Executable string `json:"executable"` } // DKMSBuild is one DKMS module at one version for one kernel. type DKMSBuild struct { Module string `json:"module"` Version string `json:"version"` Kernel string `json:"kernel"` } // RollbackItem is one package a rollback brings back (ADR 0508 §7): from its version now to the record's, and // where that version comes from. type RollbackItem struct { Name string `json:"name"` From string `json:"from"` To string `json:"to"` Source string `json:"source"` Location string `json:"location"` } // Where a rollback takes a package's version from, in the order it is looked for. const ( SourceCache = "cache" // the node's package cache: a file path SourceArchive = "archive" // the Arch Linux Archive's copy of that exact version: a URL SourceRepository = "repository" // the archive's repository as it stood on the record's date: a URL ) // records // Record is `records..`: the node's packages taken before a run and read back (ADR 0508 §2): a copy of // `installed.`, the running kernel (/proc/sys/kernel/osrelease), the units failed at that moment, and the // installed DKMS modules. type Record struct { Node string `json:"node"` Run string `json:"run"` At time.Time `json:"at"` Installed InstalledValue `json:"installed"` Kernel string `json:"kernel"` FailedUnits []string `json:"failed_units"` DKMS []DKMSModule `json:"dkms"` } // DKMSModule is one installed DKMS module and the kernels it is built for. type DKMSModule struct { Module string `json:"module"` Version string `json:"version"` Kernels []string `json:"kernels"` } // rounds // Round is `rounds.`: the week's round, created once (a create, so one round per week) by the first node // of the order when it opens it, with the order as it stood then (ADR 0508 §5). type Round struct { Round string `json:"round"` Opened time.Time `json:"opened"` OpenedBy string `json:"opened_by"` Order []string `json:"order"` } // RoundNode is `rounds..`: the node's place in the round. Run is the run it started in its turn, // written by the node itself; Pass, the operator's pass of a node that stopped the round, written by the keeper // of the node waiting on it (ADR 0508 §6). Both write through a compare-and-set. type RoundNode struct { Round string `json:"round"` Node string `json:"node"` Run string `json:"run,omitempty"` Pass *Pass `json:"pass,omitempty"` } // Pass is a pass of a node asked of the operator through the operator's channel (ADR 0259): the reason, the // channel's ask, and its answer once given (Answer empty while it waits). type Pass struct { Reason string `json:"reason"` Ask string `json:"ask"` Asked time.Time `json:"asked"` Answer string `json:"answer,omitempty"` Answered *time.Time `json:"answered,omitempty"` } // The operator's answers to a pass. const ( PassApproved = "approved" PassDeclined = "declined" ) // asks // The acts an ask asks. const ( ActUpgrade = "upgrade" // pacman_upgrade ActRollback = "rollback" // pacman_rollback with apply ActPass = "pass" // pacman_round with pass ) // Ask is `asks..`: an act asked of a node, taken by that node's keeper alone, which answers in `runs` // (an upgrade, a rollback) or in `rounds` (a pass) and then deletes the ask (ADR 0508 §8). A rollback names the // run whose record it returns to and whether the packages the upgrade added are removed; a pass names the // round, the node passed and the reason, and is asked of the node waiting on it. type Ask struct { ID string `json:"id"` Node string `json:"node"` Act string `json:"act"` At time.Time `json:"at"` Of string `json:"of,omitempty"` RemoveAdded bool `json:"remove_added,omitempty"` Round string `json:"round,omitempty"` Pass string `json:"pass,omitempty"` Reason string `json:"reason,omitempty"` } // events // PackageChanged is `package.changed`: a write of `installed.` that changed it (ADR 0507 §3). type PackageChanged struct { Node string `json:"node"` At time.Time `json:"at"` Added []PackageVersion `json:"added"` Removed []PackageVersion `json:"removed"` Changed []VersionChange `json:"changed"` } // UpgradeRecorded is `upgrade.recorded`: the run's record is written and read back. type UpgradeRecorded struct { Node string `json:"node"` Run string `json:"run"` Packages int `json:"packages"` Kernel string `json:"kernel"` } // KernelHeld is `upgrade.kernel-held`: kernels left out of a run, each with the module and bound that hold it. type KernelHeld struct { Node string `json:"node"` Run string `json:"run"` Held []HeldKernel `json:"held"` } // UpgradeStarted is `upgrade.started`: a run's transaction started. type UpgradeStarted struct { Node string `json:"node"` Run string `json:"run"` From string `json:"from"` Round string `json:"round,omitempty"` Unit string `json:"unit"` Ignored []string `json:"ignored"` } // UpgradeFinished is `upgrade.finished`: a run's transaction exited 0, with what it changed. type UpgradeFinished struct { Node string `json:"node"` Run string `json:"run"` Changes Changes `json:"changes"` } // UpgradeFailed is `upgrade.failed`: a run that did not start (Step StepRefused, with why in Error) or whose // transaction failed (Step StepFailed, with its exit status and last error line). type UpgradeFailed struct { Node string `json:"node"` Run string `json:"run"` Step string `json:"step"` Exit *int `json:"exit,omitempty"` Error string `json:"error"` } // UpgradeChecked is `upgrade.checked`: the after-upgrade check of a run, an upgrade's or a rollback's; the // check whole is in the run. type UpgradeChecked struct { Node string `json:"node"` Run string `json:"run"` Kind string `json:"kind"` Passed bool `json:"passed"` } // RollbackStarted is `rollback.started`: a rollback's transaction started, returning to the record of run Of. type RollbackStarted struct { Node string `json:"node"` Run string `json:"run"` Of string `json:"of"` Unit string `json:"unit"` } // RollbackFinished is `rollback.finished`: a rollback's transaction ended, with its exit status and, when it // failed, its last error line. type RollbackFinished struct { Node string `json:"node"` Run string `json:"run"` Of string `json:"of"` Exit int `json:"exit"` Error string `json:"error,omitempty"` } // The keeper's config file // The keeper's environment, named by the manifest's process resource. const ( EnvBroker = "PACMAN_KEEPER_BROKER" // the keeper's bus account file EnvConfig = "PACMAN_KEEPER_CONFIG" // the keeper's config file, a KeeperConfig ) // KeeperConfig is the keeper's config file: the module's config values the mesh merges into it (ADR 0348 §5). // The process reads it at its start; the mesh restarts the process when it changes (the manifest's // `restart-on`). type KeeperConfig struct { // UpgradeOrder is the nodes in the order they upgrade; the first opens each week's round. Empty: no round. UpgradeOrder []string `json:"upgrade-order"` // RecordDays is how long a record is kept, in days. RecordDays int `json:"record-days"` // RoundStartMinutes is how long a node whose turn came may wait before starting, in minutes, before the // round is said to stand still. RoundStartMinutes int `json:"round-start-minutes"` }