The keeper process runs under its own account with a bus account of its own, keeps the six states and emits the nine events of ADR 0507 and ADR 0508; its config (upgrade-order, record-days, round-start-minutes) lands in its config file and restarts it. shapes.go fixes every key's value and event body as the interface the keeper and the verbs share. The weekly timer and its service are dropped, which the node-engine removes.
202 lines
7.7 KiB
Go
202 lines
7.7 KiB
Go
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) != 2 || env[EnvBroker] != "${dir:state}/broker-keeper.secret" || env[EnvConfig] != "${dir:state}/keeper.json" {
|
|
t.Errorf("the keeper's environment names its account file and its config file: %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" {
|
|
t.Errorf("the keeper's bus declares %s: it keeps its own states and emits its events, nothing else yet", key)
|
|
}
|
|
}
|
|
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)")
|
|
}
|
|
}
|