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::}. 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::} (%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:}` 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 }