ADR 0214 / to-be 43: a node seat whose holder keeps nightly restore points of what every module on the machine declares. A contribution of kind backup may name its module's own directories, filled per module when placed. The catalogue check refuses a store provider that contributes no backup; parsing does not, so the providers already running stay readable.
227 lines
7.9 KiB
Go
227 lines
7.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// A module contributes to a seat it does not hold (novox/hq ADR 0212).
|
|
//
|
|
// ADR 0210 made a tool's configuration its seat holder's, and every other module's way in a
|
|
// contribution to the seat. The environment, the shell's slots and the power moments each became a
|
|
// field of their own; this is the general form, so a new seat that takes contributions is a row in
|
|
// the seat table rather than a change to the manifest: a contribution names a seat, a kind that
|
|
// seat receives, and text in the tool's own grammar, which the controller never reads.
|
|
|
|
// HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5).
|
|
const HotkeysSeat = "node-hotkeys"
|
|
|
|
// MessageBusSeat is the machine's D-Bus (novox/hq ADR 0215).
|
|
const MessageBusSeat = "node-message-bus"
|
|
|
|
// BackupSeat is the machine's backups (novox/hq ADR 0214, to-be 43).
|
|
const BackupSeat = "node-backup"
|
|
|
|
// storeProvisions are the provisions whose provider keeps its consumers' data (novox/hq ADR 0214):
|
|
// a module providing one must say how to back it up, or the data the mesh hands out is the data it
|
|
// cannot restore — issue 241's seven databases. A provision that holds nothing worth keeping (a
|
|
// route, a cache, a name) is not here; adding one is adding a store.
|
|
var storeProvisions = map[string]bool{
|
|
"postgres-database": true,
|
|
"mssql-database": true,
|
|
"mongodb-database": true,
|
|
"s3-bucket": true,
|
|
"influxdb-api": true,
|
|
"secret": true,
|
|
}
|
|
|
|
// CheckBackup is a store provider that contributes no backup (novox/hq ADR 0214, "How it is
|
|
// checked").
|
|
//
|
|
// **The catalogue check's, not parsing's.** A manifest already registered and running was written
|
|
// before the rule; refusing it on read would make the controller refuse the very providers whose
|
|
// data the rule protects. New definitions meet it in the catalogue check, where they are written.
|
|
func CheckBackup(m Manifest) []string {
|
|
backs := false
|
|
for _, c := range m.Contributions {
|
|
if s, known := SeatNamed(c.Seat); known && s.Name == BackupSeat && c.Kind == "backup" {
|
|
backs = true
|
|
}
|
|
}
|
|
if backs {
|
|
return nil
|
|
}
|
|
var problems []string
|
|
for _, o := range m.Provides {
|
|
if storeProvisions[o.Name] {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s provides %s, which keeps its consumers' data, and contributes no backup to %s; a "+
|
|
"store says how its data is copied (novox/hq ADR 0214)", m.Module, o.Name, BackupSeat))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
|
|
type SeatContribution struct {
|
|
// Seat is the seat whose holder places it.
|
|
Seat string `json:"seat"`
|
|
// Kind is which of the seat's receivable kinds it is.
|
|
Kind string `json:"kind"`
|
|
// Content is the text, in the tool's own grammar. Never interpreted.
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
// ofContribution is where a holder places a kind: ${contribution:<seat>:<kind>}. Loose inside the
|
|
// braces, so a misspelt seat or kind is found and refused rather than written out as text.
|
|
var ofContribution = regexp.MustCompile(`\$\{contribution:([^}]*)\}`)
|
|
|
|
// receivable is what a seat receives of a kind, with the seat's canonical name; false when the seat
|
|
// is unknown or does not receive it.
|
|
func receivable(seat, kind string) (Seat, Receivable, bool) {
|
|
s, known := SeatNamed(seat)
|
|
if !known {
|
|
return Seat{}, Receivable{}, false
|
|
}
|
|
for _, r := range s.Receives {
|
|
if r.Kind == kind {
|
|
return s, r, true
|
|
}
|
|
}
|
|
return s, Receivable{}, false
|
|
}
|
|
|
|
// kindsOf names a seat's receivable kinds for a refusal.
|
|
func kindsOf(s Seat) string {
|
|
if len(s.Receives) == 0 {
|
|
return "it receives no contributions"
|
|
}
|
|
var kinds []string
|
|
for _, r := range s.Receives {
|
|
kinds = append(kinds, r.Kind)
|
|
}
|
|
return "it receives " + strings.Join(kinds, ", ")
|
|
}
|
|
|
|
// seatContributionProblems is what is wrong with this module's contributions, from the manifest
|
|
// alone (novox/hq ADR 0212 §2).
|
|
func (m Manifest) seatContributionProblems() []string {
|
|
var problems []string
|
|
for i, c := range m.Contributions {
|
|
s, r, ok := receivable(c.Seat, c.Kind)
|
|
// A directory a contribution names must be one of this module's own, here rather than on the
|
|
// machine — where a `${dir:x}` nobody declared would reach the holder as the literal text.
|
|
if ok && r.Dirs {
|
|
declared := map[string]string{}
|
|
for _, res := range m.Resources {
|
|
if fmt.Sprint(res["type"]) == "directory" {
|
|
declared[fmt.Sprint(res["id"])] = ""
|
|
}
|
|
}
|
|
if _, err := dirFill(c.Content, declared, m.Module); err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d: %v", m.Module, i+1, err))
|
|
}
|
|
}
|
|
switch {
|
|
case s.Name == "":
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's contribution %d is to the seat %q, which the mesh does not define", m.Module, i+1, c.Seat))
|
|
case !ok:
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's contribution %d to %s is of the kind %q; %s (novox/hq ADR 0212)",
|
|
m.Module, i+1, s.Name, c.Kind, kindsOf(s)))
|
|
}
|
|
if strings.TrimSpace(c.Content) == "" {
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d has no content", m.Module, i+1))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// seatPlaceholderProblems is what is wrong with one resource's ${contribution:…}: placed only in a
|
|
// file's content, naming a seat and a kind it receives, and only by a module that claims that seat
|
|
// — another would be a second writer of a file there is one of (novox/hq ADR 0212 §3).
|
|
func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
|
|
var problems []string
|
|
for _, field := range sortedKeys(r) {
|
|
v, ok := r[field].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
found := ofContribution.FindAllStringSubmatch(v, -1)
|
|
if len(found) == 0 {
|
|
continue
|
|
}
|
|
if field != "content" {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s in its %s; contributions are placed only in a file's content",
|
|
m.Module, r["id"], found[0][0], field))
|
|
continue
|
|
}
|
|
for _, f := range found {
|
|
seat, kind, two := strings.Cut(f[1], ":")
|
|
s, _, ok := receivable(seat, kind)
|
|
if !two || !ok {
|
|
detail := "the mesh defines no seat " + fmt.Sprintf("%q", seat)
|
|
if s.Name != "" {
|
|
detail = s.Name + ": " + kindsOf(s)
|
|
}
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s; a contribution is ${contribution:<seat>:<kind>} (%s)",
|
|
m.Module, r["id"], f[0], detail))
|
|
continue
|
|
}
|
|
if !m.ClaimsSeat(s.Name) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; every module's contributions to a "+
|
|
"seat are placed by its holder alone (novox/hq ADR 0212)",
|
|
m.Module, r["id"], f[0], m.Module, s.Name))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
|
|
// in module order, each module's pieces in the order it declared them, each module's preceded by a
|
|
// comment line naming it in the tool's grammar, and empty when nothing is contributed. A kind that
|
|
// takes directories has each contributor's `${dir:<id>}` filled with where that module's directory
|
|
// is on this machine (novox/hq to-be 43).
|
|
func seatContributions(modules []Manifest, seat, kind string, with Rendering) (string, error) {
|
|
s, r, ok := receivable(seat, kind)
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
var failed error
|
|
var b strings.Builder
|
|
for _, m := range inModuleOrder(modules) {
|
|
named := false
|
|
for _, c := range m.Contributions {
|
|
if c.Kind != kind {
|
|
continue
|
|
}
|
|
if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name {
|
|
continue
|
|
}
|
|
if !named {
|
|
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
|
|
named = true
|
|
}
|
|
content := c.Content
|
|
if r.Dirs {
|
|
filled, err := dirFill(content, dirsFor(m, with), m.Module)
|
|
if err != nil && failed == nil {
|
|
failed = err
|
|
}
|
|
content = filled
|
|
}
|
|
b.WriteString(content)
|
|
if !strings.HasSuffix(content, "\n") {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
return b.String(), failed
|
|
}
|