Files
mesh-catalog/modules/pacman/cmd/pacman-tools/keeper_manifest_test.go
T

214 lines
8.5 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) != 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)")
}
}