package main // The manifest of the keeper (novox/hq ADR 0507 §1-§3, §5; ADR 0508 §1, §2, §5, §6, §8; acceptance 1841/AC12): // the module's background work is a process of its own under an account the module makes, with a bus account of // its own, the module's six states and the nine events it emits; the module's config values land in the // keeper's config file, which restarts it; the weekly timer and its service are gone (a dropped file or service // is removed by the node-engine); `upgrade-order` falls back to the mesh-level key of that name. import ( "bytes" "encoding/json" "os" "path/filepath" "reflect" "strings" "testing" ) // rawManifest is the manifest as written, every field kept. func rawManifest(t *testing.T) map[string]any { t.Helper() raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json")) if err != nil { t.Fatal(err) } var m map[string]any if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } return m } func strs(v any) []string { var out []string l, _ := v.([]any) for _, x := range l { s, _ := x.(string) out = append(out, s) } return out } func TestTheWeeklyTimerAndItsServiceAreGone(t *testing.T) { m := manifest(t) for _, r := range m.Resources { id, _ := r["id"].(string) path, _ := r["path"].(string) unit, _ := r["unit"].(string) if id == "weekly-upgrade" || id == "upgrade-timer" || id == "upgrade-service" || strings.Contains(path, "weekly-upgrade") || strings.Contains(unit, "weekly-upgrade") { t.Errorf("the weekly upgrade is replaced by the round (ADR 0508 §5), and %v still declares it", id) } } for _, name := range []string{"mesh-pacman-weekly-upgrade.timer", "mesh-pacman-weekly-upgrade.service"} { if _, err := os.Stat(filepath.Join(moduleDir, "files", name)); err == nil { t.Errorf("files/%s is still shipped", name) } } } func TestTheKeeperIsAProcessUnderItsOwnAccountWithABusAccountOfItsOwn(t *testing.T) { m := manifest(t) user := m.resource(t, "keeper-account") if user["type"] != "user" || user["name"] != "pacman-keeper" || user["shell"] != "/usr/bin/nologin" { t.Fatalf("the keeper's account: %v", user) } k := m.resource(t, "keeper") if k["type"] != "process" || k["name"] != "pacman-keeper" || k["artifact"] != "tools" || strings.Join(strs(k["run"]), " ") != "./"+binaryName+" keeper" { t.Fatalf("the keeper: %v", k) } if k["user"] != "pacman-keeper" { t.Errorf("the keeper runs as its own account, never root (ADR 0508 §8): user %v", k["user"]) } env, _ := k["env"].(map[string]any) if len(env) != 3 || env[EnvBroker] != "${dir:state}/broker-keeper.secret" || env[EnvConfig] != "${dir:state}/keeper.json" || env[EnvRootSocket] != RootSocketPath { t.Errorf("the keeper's environment names its account file, its config file and the root helper's socket: %v", env) } secrets, _ := rawManifest(t)["own-secrets"].(map[string]any) broker, _ := secrets["broker-keeper"].(map[string]any) if broker["path"] != env[EnvBroker] || broker["owner"] != "pacman-keeper" || len(secrets) != 1 { t.Errorf("the bus account is the keeper's own secret, owned by its account: %v", secrets) } state := m.resource(t, "state") if state["type"] != "directory" || state["place"] != "." || state["unknown"] == nil { t.Errorf("the state directory: %v", state) } order := map[string]int{} for i, r := range m.Resources { order[r["id"].(string)] = i } if order["keeper-account"] > order["keeper"] || order["state"] > order["keeper-config"] || order["keeper-config"] > order["keeper"] { t.Errorf("the account, the directory and the config file come before the process: %v", order) } } func TestTheKeeperDeclaresTheModulesStatesAndEvents(t *testing.T) { raw := rawManifest(t) k := manifest(t).resource(t, "keeper") bus, _ := k["bus"].(map[string]any) var events []string for _, e := range Events { events = append(events, e.Name) } if got := strs(bus["state"]); !reflect.DeepEqual(got, States) { t.Errorf("the keeper's states: %v, want %v", got, States) } if got := strs(bus["emits"]); !reflect.DeepEqual(got, events) { t.Errorf("the keeper's events: %v, want %v", got, events) } for key := range bus { if key != "state" && key != "emits" && key != "asks" { t.Errorf("the keeper's bus declares %s: it keeps its own states, emits its events and reads its asks' answers, nothing else", key) } } // A pass is answered on the operator's channel: the tools ask, the keeper reads the channel's record of its own // asks (ADR 0508 §6, ADR 0259, ADR 0436 §1). if bus["asks"] != true || strings.Join(strs(raw["uses"]), " ") != "operator-channel" { t.Errorf("the keeper reads its asks' answers and the module uses the operator's channel: %v %v", bus["asks"], raw["uses"]) } // The tools read round-start-minutes from the keeper's config file. for _, a := range manifest(t).Build.Artifacts { if env, _ := a["env"].(map[string]any); a["name"] == "tools" && env[EnvConfig] != "${dir:state}/keeper.json" { t.Errorf("the tools bundle is not named the keeper's config file: %v", a["env"]) } } if got := strs(raw["state"]); !reflect.DeepEqual(got, States) { t.Errorf("the module's states: %v, want %v", got, States) } if got := strs(raw["emits"]); !reflect.DeepEqual(got, events) { t.Errorf("the module's events: %v, want %v", got, events) } // The verbs read which packages each node's modules declare; the process reads no other module's state // (ADR 0507 §5, ADR 0436 §1). if got := strs(raw["reads"]); !reflect.DeepEqual(got, []string{"mesh-controller.declared-packages"}) { t.Errorf("reads: %v", got) } } // configValue is the module's config value of that key. func configValue(t *testing.T, key string) map[string]any { t.Helper() config, _ := rawManifest(t)["config"].(map[string]any) v, _ := config[key].(map[string]any) if v == nil { t.Fatalf("no config value %s", key) } if d, _ := v["description"].(string); len(d) < 40 { t.Errorf("%s says what it means: %q", key, d) } return v } func TestTheConfigValuesLandInTheKeepersConfigFileWhichRestartsIt(t *testing.T) { order := configValue(t, "upgrade-order") if order["type"] != "list" || order["trusted"] != true || order["predefined"] != "upgrade-order" || !reflect.DeepEqual(order["default"], []any{}) || order["required"] != nil { t.Errorf("upgrade-order is a trusted list falling back to the mesh-level key, empty by default: %v", order) } for key, def := range map[string]float64{"record-days": 180, "round-start-minutes": 120} { v := configValue(t, key) if v["type"] != "number" || v["default"] != def || v["minimum"] != float64(1) { t.Errorf("%s is a number of at least 1, %v by default: %v", key, def, v) } } config, _ := rawManifest(t)["config"].(map[string]any) if len(config) != 3 { t.Errorf("config: %v", config) } m := manifest(t) f := m.resource(t, "keeper-config") if f["type"] != "file" || f["path"] != "${dir:state}/keeper.json" || f["merge"] != "json" || f["trusted"] != true || f["mode"] != "0644" { t.Fatalf("the keeper's config file: %v", f) } if got := strs(f["takes-config"]); strings.Join(got, " ") != "upgrade-order record-days round-start-minutes" { t.Errorf("the file takes every config value: %v", got) } if got := strs(m.resource(t, "keeper")["restart-on"]); strings.Join(got, " ") != "keeper-config" { t.Errorf("a changed config file restarts the keeper, which reads it at its start: %v", got) } // The template is the file a node starts with: the defaults, read as the keeper reads them. dec := json.NewDecoder(bytes.NewReader([]byte(templateText(t, f)))) dec.DisallowUnknownFields() var c KeeperConfig if err := dec.Decode(&c); err != nil { t.Fatalf("the keeper cannot read its config file: %v", err) } if c.UpgradeOrder == nil || len(c.UpgradeOrder) != 0 || c.RecordDays != 180 || c.RoundStartMinutes != 120 { t.Errorf("the template holds the defaults: %+v", c) } } func TestUpgradeOrderIsATrustedListOfTheMesh(t *testing.T) { raw, err := os.ReadFile(filepath.Join(moduleDir, "..", "..", "predefined.json")) if err != nil { t.Fatal(err) } var keys map[string]map[string]any if err := json.Unmarshal(raw, &keys); err != nil { t.Fatal(err) } k := keys["upgrade-order"] if k["type"] != "list" || k["trusted"] != true { t.Fatalf("predefined upgrade-order: %v", k) } if d, _ := k["description"].(string); !strings.Contains(d, "order") || !strings.Contains(d, "round") { t.Errorf("its description says the nodes' order and the round: %q", d) } if k["type"] != configValue(t, "upgrade-order")["type"] { t.Error("a value falls back only to a key of its own type (ADR 0348 §4)") } }