package catalogue import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-host/validate" ) // novox/hq ADR 0366: a module that runs as its own account declares its runtime's health under one manifest // key, `runtime-health`, in the form a resource's `health` takes; the controller puts it on the process it // writes for that runtime, and a tool check there stands beside the node-engine's judgement that the runtime // is alive. A module that runs as no account of its own is refused the key. // ownAccountManifest is a module of its own account with nothing long-running but its own runtime, given // runtime-health (or none, when health is nil), and running as no account when runsAs is false. func ownAccountManifest(t *testing.T, runsAs bool, health any) []byte { t.Helper() m := map[string]any{"module": "keeper", "tools": []any{"keeper_status", "keeper_health"}, "own-secrets": map[string]any{"broker": map[string]any{"path": "/var/lib/keeper/broker"}}, "resources": []any{map[string]any{"id": "account", "type": "user", "name": "keeper"}}} if runsAs { m["runs-as"], m["secrets-owner"] = "keeper", "keeper" } if health != nil { m["runtime-health"] = health } raw, err := json.Marshal(m) if err != nil { t.Fatal(err) } return raw } func TestAModuleOfItsOwnAccountDeclaresItsRuntimesHealthByItsOwnTool(t *testing.T) { m, err := ParseManifest(ownAccountManifest(t, true, map[string]any{"kind": "tool", "tool": "keeper_health"})) if err != nil { t.Fatalf("a tool check on its own runtime was refused: %v", err) } if got := HealthChecks(m); len(got) != 1 || got[0] != "keeper.keeper_health" { t.Errorf("the engine is granted %v, want keeper.keeper_health", got) } if r, has := OwnRuntimeHealth(m); !has || r["id"] != "keeper."+OwnRuntimeID() { t.Errorf("its runtime's health reads as %v %v", r, has) } } func TestRuntimeHealthIsRefusedWithoutAnAccountOfItsOwn(t *testing.T) { _, err := ParseManifest(ownAccountManifest(t, false, map[string]any{"kind": "tool", "tool": "keeper_health"})) if err == nil || !strings.Contains(err.Error(), "runtime-health") || !strings.Contains(err.Error(), "runs-as") { t.Errorf("runtime-health without runs-as: %v", err) } } func TestARuntimeHealthIsHeldToTheHealthRules(t *testing.T) { for _, c := range []struct { health any says string }{ {map[string]any{"kind": "tool", "tool": "keeper_restart"}, "does not serve"}, {map[string]any{"kind": "tool", "tool": "keeper_health", "interval": "2s"}, "no more often"}, {map[string]any{"kind": "exec", "command": "true"}, "only a container"}, {map[string]any{"kind": "tool", "tool": "keeper_health", "port": 80}, "names a port"}, {"tool", "is an object"}, } { _, err := ParseManifest(ownAccountManifest(t, true, c.health)) if err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%v: %v, want it refused saying %q", c.health, err, c.says) } } } // The runtime-health is composed onto .own-runtime, in the words the node-engine takes; to an engine // that does not read health it is not sent, as for every resource. func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) { goRuntime := Manifest{Module: RuntimeModule, Version: "1", OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}}, Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } keeper := aToolsModule(t, "keeper", "tools/index.js") keeper.Tools = []string{"keeper_health"} keeper.RunsAs, keeper.SecretsOwner = "keeper", "keeper" keeper.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/keeper/broker"}} keeper.RuntimeHealth = map[string]any{"kind": "tool", "tool": "keeper_health", "interval": "60s", "timeout": "10s", "looks": 2.0} for _, reads := range []bool{true, false} { with := Rendering{ArtifactStore: "anchor.internal:5101", ReadsHealth: reads, Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed"}, "keeper": {"broker": "own"}}} out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{keeper, goRuntime}}.Declaration(with) if err != nil { t.Fatal(err) } own := fileNamed(out, "keeper."+OwnRuntimeID()) if own == nil { t.Fatalf("no own runtime: %v", ids(out)) } h, has := own[HealthField].(map[string]any) if !reads { if has { t.Errorf("health sent to an engine that does not read it: %v", h) } continue } if !has || h["kind"] != "tool" || h["tool"] != "keeper_health" || h["interval"] != "1m0s" || h["timeout"] != "10s" || h["looks"] != 2 || h["grace"] != "1m0s" { t.Errorf("its own runtime's health: %v", own[HealthField]) } if machine := fileNamed(out, RuntimeModule+"."+RuntimeProcessID()); machine[HealthField] != nil { t.Errorf("the machine's runtime was given the module's health: %v", machine[HealthField]) } body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": out}) if err != nil { t.Fatal(err) } if problems := validate.Declaration(body); len(problems) > 0 { t.Errorf("composed into something the node-engine refuses: %v", problems) } } // The catalogue's own manifest is not written into. if _, ok := keeper.RuntimeHealth.(map[string]any)["grace"]; ok { t.Error("composing wrote the defaults into the catalogue's manifest") } }