The node-backup seat: a module contributes its backup, the mesh fills its directories
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.
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A store provider says how its data is backed up (novox/hq ADR 0214); a provider of something that
|
||||||
|
// holds nothing does not have to.
|
||||||
|
func TestAStoreProviderWithoutABackupIsRefusedByTheCheck(t *testing.T) {
|
||||||
|
bare, err := ParseManifest([]byte(`{"module":"pg","version":"1",
|
||||||
|
"provides":[{"name":"postgres-database","scope":"mesh"}]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parsing refused it, and the rule is the check's: %v", err)
|
||||||
|
}
|
||||||
|
if problems := CheckBackup(bare); len(problems) != 1 || !strings.Contains(problems[0], "node-backup") {
|
||||||
|
t.Fatalf("a store with no backup passed the check: %v", problems)
|
||||||
|
}
|
||||||
|
backed, err := ParseManifest([]byte(`{"module":"pg","version":"1",
|
||||||
|
"provides":[{"name":"postgres-database","scope":"mesh"}],
|
||||||
|
"resources":[{"id":"dumps","type":"directory","mode":"0700"}],
|
||||||
|
"contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:dumps}"}]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if problems := CheckBackup(backed); len(problems) != 0 {
|
||||||
|
t.Fatalf("a store that contributes a backup was refused: %v", problems)
|
||||||
|
}
|
||||||
|
route, _ := ParseManifest([]byte(`{"module":"r","version":"1","provides":[{"name":"route","scope":"mesh"}]}`))
|
||||||
|
if problems := CheckBackup(route); len(problems) != 0 {
|
||||||
|
t.Fatalf("a route was asked for a backup: %v", problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A backup contribution names its module's own directories; one it does not declare is refused
|
||||||
|
// where it is written rather than reaching the holder as the literal text.
|
||||||
|
func TestABackupNamingAnUndeclaredDirectoryIsRefused(t *testing.T) {
|
||||||
|
_, err := ParseManifest([]byte(`{"module":"pg","version":"1",
|
||||||
|
"contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:nowhere}"}]}`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "nowhere") {
|
||||||
|
t.Fatalf("a backup of an undeclared directory was accepted: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The holder receives every module's backup lines with each module's own directories filled, each
|
||||||
|
// module's under a comment naming it — and a kind written in a shell's grammar is left untouched.
|
||||||
|
func TestBackupContributionsArePlacedWithTheirModulesDirectories(t *testing.T) {
|
||||||
|
pg, err := ParseManifest([]byte(`{"module":"pg","version":"1",
|
||||||
|
"resources":[{"id":"dumps","type":"directory","path":"/srv/pg/dumps","mode":"0700"}],
|
||||||
|
"contributions":[{"seat":"node-backup","kind":"backup","content":"run pg-dump-all\npath ${dir:dumps}"}]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mail, err := ParseManifest([]byte(`{"module":"mail","version":"1",
|
||||||
|
"resources":[{"id":"spool","type":"directory","mode":"0700"}],
|
||||||
|
"contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:spool}"}]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
placed, err := seatContributions([]Manifest{pg, mail}, BackupSeat, "backup", Rendering{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"# pg\nrun pg-dump-all\npath /srv/pg/dumps\n", "# mail\npath /var/lib/mail/spool\n"} {
|
||||||
|
if !strings.Contains(placed, want) {
|
||||||
|
t.Errorf("placed contributions lack %q:\n%s", want, placed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(placed, "${dir:") {
|
||||||
|
t.Errorf("a directory reached the holder unfilled:\n%s", placed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,3 +86,36 @@ func TestNoCatalogueManifestNamesAnInstallation(t *testing.T) {
|
|||||||
t.Fatalf("%d value(s) name an installation:\n %s", len(named), strings.Join(named, "\n "))
|
t.Fatalf("%d value(s) name an installation:\n %s", len(named), strings.Join(named, "\n "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEveryCatalogueStoreSaysHowItIsBackedUp is ADR 0214's check over the real catalogue: a module
|
||||||
|
// providing a store contributes a backup to node-backup, so a store added is a store backed up.
|
||||||
|
func TestEveryCatalogueStoreSaysHowItIsBackedUp(t *testing.T) {
|
||||||
|
root := catalogueRoot(t)
|
||||||
|
found, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||||
|
if err != nil || len(found) == 0 {
|
||||||
|
t.Fatalf("no manifests under %s: %v", root, err)
|
||||||
|
}
|
||||||
|
stores := 0
|
||||||
|
for _, p := range found {
|
||||||
|
raw, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", p, err)
|
||||||
|
}
|
||||||
|
m, err := ParseManifest(raw)
|
||||||
|
if err != nil {
|
||||||
|
continue // TestEveryCatalogueManifestParses says why
|
||||||
|
}
|
||||||
|
for _, o := range m.Provides {
|
||||||
|
if storeProvisions[o.Name] {
|
||||||
|
stores++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, problem := range CheckBackup(m) {
|
||||||
|
t.Error(problem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stores == 0 {
|
||||||
|
t.Fatal("no module in the catalogue provides a store, so this proved nothing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
// seat that places them (novox/hq ADR 0203, ADR 0204). Gathered from every module on
|
// seat that places them (novox/hq ADR 0203, ADR 0204). Gathered from every module on
|
||||||
// the node, as the jails are, and **last of every placeholder pass**: shell code is a
|
// the node, as the jails are, and **last of every placeholder pass**: shell code is a
|
||||||
// shell's own syntax, full of `${…}` no pass above should ever be shown.
|
// shell's own syntax, full of `${…}` no pass above should ever be shown.
|
||||||
if err := contributionsInto(copied, m, r.Modules, thisMachine); err != nil {
|
if err := contributionsInto(copied, m, r.Modules, thisMachine, with); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
|
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ func shellCode(modules []Manifest, shell, slot string) string {
|
|||||||
// `${machine:…}` some module wrote for its shell to see. So nothing runs after them, the environment
|
// `${machine:…}` some module wrote for its shell to see. So nothing runs after them, the environment
|
||||||
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
|
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
|
||||||
// wrote, so a contributed piece is never scanned again.
|
// wrote, so a contributed piece is never scanned again.
|
||||||
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string) error {
|
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string, with Rendering) error {
|
||||||
if problems := append(placeholderProblems(m, resource), seatPlaceholderProblems(m, resource)...); len(problems) > 0 {
|
if problems := append(placeholderProblems(m, resource), seatPlaceholderProblems(m, resource)...); len(problems) > 0 {
|
||||||
return fmt.Errorf("%s", problems[0])
|
return fmt.Errorf("%s", problems[0])
|
||||||
}
|
}
|
||||||
@@ -571,14 +571,22 @@ func contributionsInto(resource map[string]any, m Manifest, modules []Manifest,
|
|||||||
// the controller does not read, so neither may be scanned after the other is in place — a
|
// the controller does not read, so neither may be scanned after the other is in place — a
|
||||||
// contributed line that happened to spell the other's placeholder would be filled.
|
// contributed line that happened to spell the other's placeholder would be filled.
|
||||||
if ofContributed.MatchString(content) {
|
if ofContributed.MatchString(content) {
|
||||||
|
var failed error
|
||||||
content = ofContributed.ReplaceAllStringFunc(content, func(placeholder string) string {
|
content = ofContributed.ReplaceAllStringFunc(content, func(placeholder string) string {
|
||||||
found := ofContributed.FindStringSubmatch(placeholder)
|
found := ofContributed.FindStringSubmatch(placeholder)
|
||||||
first, second, _ := strings.Cut(found[2], ":")
|
first, second, _ := strings.Cut(found[2], ":")
|
||||||
if found[1] == "contribution" {
|
if found[1] == "contribution" {
|
||||||
return seatContributions(modules, first, second)
|
placed, err := seatContributions(modules, first, second, with)
|
||||||
|
if err != nil && failed == nil {
|
||||||
|
failed = err
|
||||||
|
}
|
||||||
|
return placed
|
||||||
}
|
}
|
||||||
return shellCode(modules, first, second)
|
return shellCode(modules, first, second)
|
||||||
})
|
})
|
||||||
|
if failed != nil {
|
||||||
|
return failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resource["content"] = content
|
resource["content"] = content
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -20,6 +20,49 @@ const HotkeysSeat = "node-hotkeys"
|
|||||||
// MessageBusSeat is the machine's D-Bus (novox/hq ADR 0215).
|
// MessageBusSeat is the machine's D-Bus (novox/hq ADR 0215).
|
||||||
const MessageBusSeat = "node-message-bus"
|
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.
|
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
|
||||||
type SeatContribution struct {
|
type SeatContribution struct {
|
||||||
// Seat is the seat whose holder places it.
|
// Seat is the seat whose holder places it.
|
||||||
@@ -66,7 +109,20 @@ func kindsOf(s Seat) string {
|
|||||||
func (m Manifest) seatContributionProblems() []string {
|
func (m Manifest) seatContributionProblems() []string {
|
||||||
var problems []string
|
var problems []string
|
||||||
for i, c := range m.Contributions {
|
for i, c := range m.Contributions {
|
||||||
s, _, ok := receivable(c.Seat, c.Kind)
|
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 {
|
switch {
|
||||||
case s.Name == "":
|
case s.Name == "":
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, fmt.Sprintf(
|
||||||
@@ -129,12 +185,15 @@ func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
|
|||||||
|
|
||||||
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
|
// 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
|
// 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.
|
// comment line naming it in the tool's grammar, and empty when nothing is contributed. A kind that
|
||||||
func seatContributions(modules []Manifest, seat, kind string) string {
|
// 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)
|
s, r, ok := receivable(seat, kind)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return "", nil
|
||||||
}
|
}
|
||||||
|
var failed error
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, m := range inModuleOrder(modules) {
|
for _, m := range inModuleOrder(modules) {
|
||||||
named := false
|
named := false
|
||||||
@@ -149,11 +208,19 @@ func seatContributions(modules []Manifest, seat, kind string) string {
|
|||||||
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
|
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
|
||||||
named = true
|
named = true
|
||||||
}
|
}
|
||||||
b.WriteString(c.Content)
|
content := c.Content
|
||||||
if !strings.HasSuffix(c.Content, "\n") {
|
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")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String(), failed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ type Seat struct {
|
|||||||
type Receivable struct {
|
type Receivable struct {
|
||||||
Kind string
|
Kind string
|
||||||
Comment string
|
Comment string
|
||||||
|
// Dirs says a contribution of this kind may name its contributor's own directories as
|
||||||
|
// `${dir:<id>}`, filled with where they are on the machine before the holder places it (novox/hq
|
||||||
|
// to-be 43): a module saying which of its data to back up names a directory the mesh placed, and
|
||||||
|
// only the mesh knows where. Off for every kind written in a tool's grammar that has its own
|
||||||
|
// `${…}` — a shell's — where the mesh filling one would change what the tool reads.
|
||||||
|
Dirs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
|
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
|
||||||
@@ -221,6 +227,13 @@ var defaultSeats = append([]Seat{
|
|||||||
// system service, never restarts it live, and publishes curated events about it, never its traffic.
|
// system service, never restarts it live, and publishes curated events about it, never its traffic.
|
||||||
// It receives nothing yet: packages ship their own policies.
|
// It receives nothing yet: packages ship their own policies.
|
||||||
{Name: MessageBusSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0215"},
|
{Name: MessageBusSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0215"},
|
||||||
|
// The machine's backups (novox/hq ADR 0214, to-be 43): its holder keeps nightly restore points of
|
||||||
|
// the data every module on the machine declares, on the machine, against mistakes rather than
|
||||||
|
// disasters. A module contributes `backup` lines — what to run to take a consistent copy, and
|
||||||
|
// which of its directories to keep.
|
||||||
|
{Name: BackupSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0214",
|
||||||
|
Serves: backupVerbs(),
|
||||||
|
Receives: []Receivable{{Kind: "backup", Comment: "#", Dirs: true}}},
|
||||||
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
|
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
|
||||||
// model change, not a rename, so it stays until that is built.
|
// model change, not a rename, so it stays until that is built.
|
||||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||||
@@ -537,6 +550,27 @@ func serviceManagerVerbs() []Verb {
|
|||||||
// loginShellVerbs is the contract every holder of node-login-shell serves (novox/hq ADR 0176, ADR
|
// loginShellVerbs is the contract every holder of node-login-shell serves (novox/hq ADR 0176, ADR
|
||||||
// 0204): one command, run the way the operator's own terminal would run it, bounded below the
|
// 0204): one command, run the way the operator's own terminal would run it, bounded below the
|
||||||
// runtime's thirty-second call limit so a hung command answers rather than times the caller out.
|
// runtime's thirty-second call limit so a hung command answers rather than times the caller out.
|
||||||
|
// backupVerbs is the node-backup seat's protocol (novox/hq to-be 43): what is kept, take one now,
|
||||||
|
// and restore beside the live data — never over it.
|
||||||
|
func backupVerbs() []Verb {
|
||||||
|
return []Verb{
|
||||||
|
{Name: "backed-up", Description: "What this machine backs up: each module, what it declared, " +
|
||||||
|
"its last good night, how many restore points are kept and the repository's size.",
|
||||||
|
Input: schema(map[string]string{"module": "one module (optional)"}, nil)},
|
||||||
|
{Name: "now", Description: "Take a backup now, of one module or of every module on this " +
|
||||||
|
"machine — before a migration, a retirement or anything else that could go wrong.",
|
||||||
|
Input: schema(map[string]string{"module": "one module (optional)"}, nil)},
|
||||||
|
{Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live " +
|
||||||
|
"data, never over it: each directory as <path>.restored-<date>. Swapping it in is a " +
|
||||||
|
"person's act. Lists the restore points when none is named.",
|
||||||
|
Input: schema(map[string]string{
|
||||||
|
"module": "the module",
|
||||||
|
"snapshot": "the restore point (from `backed-up`; the newest when omitted)",
|
||||||
|
"path": "one of the module's directories (all of them when omitted)",
|
||||||
|
}, []string{"module"})},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loginShellVerbs() []Verb {
|
func loginShellVerbs() []Verb {
|
||||||
return []Verb{
|
return []Verb{
|
||||||
{Name: "execute", Description: "Run one command on this machine as the operator account, in a " +
|
{Name: "execute", Description: "Run one command on this machine as the operator account, in a " +
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|||||||
delivered[s.Delivers] = s.Name
|
delivered[s.Delivers] = s.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Thirty-seven with node-message-bus (novox/hq ADR 0215); thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199); thirty-four
|
// Thirty-eight with node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215); thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199); thirty-four
|
||||||
// with node-hotkeys (ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the
|
// with node-hotkeys (ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the
|
||||||
// graphical session's eleven (ADR 0208); twenty-one with node-package-manager and
|
// graphical session's eleven (ADR 0208); twenty-one with node-package-manager and
|
||||||
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
|
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
|
||||||
// ADR 0204); seventeen with node-build-agent (ADR 0190). Two fewer once the retired
|
// ADR 0204); seventeen with node-build-agent (ADR 0190). Two fewer once the retired
|
||||||
// mesh-build-machine and node-dns-resolver rows go, when no registered manifest claims either.
|
// mesh-build-machine and node-dns-resolver rows go, when no registered manifest claims either.
|
||||||
if len(Seats()) != 37 {
|
if len(Seats()) != 38 {
|
||||||
t.Errorf("the mesh defines %d seats rather than 37; the set is closed, so a change here is "+
|
t.Errorf("the mesh defines %d seats rather than 38; the set is closed, so a change here is "+
|
||||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user