Files
mesh-catalog/modules/mounts/cmd/mounts/manifest_test.go
T

149 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"os"
"regexp"
"sort"
"strings"
"testing"
)
type manifest struct {
Tools []string `json:"tools"`
Requires []string `json:"requires"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
Claims []map[string]any `json:"claims"`
State []map[string]any `json:"state"`
Settings map[string]map[string]any `json:"settings"`
OwnSecrets map[string]map[string]string `json:"own-secrets"`
Resources []map[string]any `json:"resources"`
}
func readManifest(t *testing.T) (manifest, string) {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m, string(raw)
}
// The tools the bundle serves are the manifest's tools and the seat's verbs, each saying whether it reads
// or acts; the module requires the share, asks the server's seat and holds node-mounts.
func TestTheManifestNamesWhatTheBundleServesAndWhatItNeeds(t *testing.T) {
m, raw := readManifest(t)
var tools, verbs []string
for _, tl := range Tools(newSystem(t).machine()) {
if !strings.HasSuffix(tl.Description, "(r)") && !strings.HasSuffix(tl.Description, "(a)") {
t.Errorf("%s does not say whether it reads (r) or acts (a)", tl.Name)
}
if v, ok := strings.CutPrefix(tl.Name, Seat+"."); ok {
verbs = append(verbs, v)
} else {
tools = append(tools, tl.Name)
}
}
sort.Strings(tools)
sort.Strings(m.Tools)
if strings.Join(tools, ",") != strings.Join(m.Tools, ",") {
t.Errorf("served %v, listed %v", tools, m.Tools)
}
if len(m.Claims) != 1 || m.Claims[0]["name"] != Seat || m.Claims[0]["scope"] != "node" {
t.Fatalf("claims %v", m.Claims)
}
var serves []string
for _, v := range m.Claims[0]["serves"].([]any) {
serves = append(serves, v.(string))
}
sort.Strings(serves)
sort.Strings(verbs)
if strings.Join(serves, ",") != strings.Join(verbs, ",") || strings.Join(verbs, ",") != "adopt,list,mount,test,unmount" {
t.Errorf("the seat serves %v, the bundle %v", serves, verbs)
}
if strings.Join(m.Requires, ",") != "nfs-share" {
t.Errorf("requires %v", m.Requires)
}
if strings.Join(m.Reads, ",") != ExportsState {
t.Errorf("the module reads nfs-server's exports from the bus: reads %v", m.Reads)
}
// It calls one verb: the controller's, for where the mesh keeps data on this machine (review finding 4d).
if strings.Join(m.Invokes, ",") != DataVerb {
t.Errorf("invokes %v, want %s", m.Invokes, DataVerb)
}
if !strings.Contains(raw, `node=${machine:name}`) {
t.Error("the config gives the process this machine's name, so it never mounts a share onto the machine that serves it")
}
if len(m.State) != 1 || m.State[0]["name"] != SharesState || m.State[0]["per-machine"] != true {
t.Errorf("state %v", m.State)
}
sec := m.OwnSecrets["smb-credentials"]
if sec["issued-by"] != "outside" || sec["taken"] != "at-start" || !strings.HasPrefix(sec["path"], "${dir:state}/") {
t.Errorf("the SMB logins are an outside party's, given with secret accept: %v", sec)
}
for _, key := range []string{"shares", "sources"} {
s := m.Settings[key]
if s["kind"] != "preference" || s["default"] != None || s["why"] == "" || !strings.Contains(raw, "${setting:"+key+"}") {
t.Errorf("setting %s: %v", key, s)
}
}
// A machine's or a person's name, as a word: "novox/hq" names the record, not the machine.
if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen|recalbox)\b|/home/|/storage`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" {
t.Errorf("the manifest says %q", hit)
}
if strings.Contains(raw, "/etc/fstab") {
t.Error("the manifest places nothing in /etc/fstab: the operator's lines are taken over only by adopt")
}
}
// Every long-running resource says how it is healthy (ADR 0240): apply by its unit, watch by mounts_health,
// both as root; the config file the process reads is the one the manifest places.
func TestTheProcessesAreRootAndJudgedByTheirUnitAndTheHealthTool(t *testing.T) {
m, raw := readManifest(t)
byID := map[string]map[string]any{}
for _, r := range m.Resources {
byID[r["id"].(string)] = r
if r["type"] == "process" || r["type"] == "service" || r["type"] == "container" {
if r["health"] == nil {
t.Errorf("%s runs and does not say how it is healthy", r["id"])
}
}
}
apply, watch := byID["apply"], byID["watch"]
if h, _ := apply["health"].(map[string]any); apply["user"] != nil || h["kind"] != "unit" {
t.Errorf("apply runs as root and is judged by its unit: %v", apply)
}
if h, _ := watch["health"].(map[string]any); watch["user"] != nil || h["kind"] != "tool" || h["tool"] != "mounts_health" {
t.Errorf("watch runs as root and is judged by mounts_health: %v", watch)
}
for id, mode := range map[string]string{"apply": "apply", "watch": "watch"} {
if run, _ := byID[id]["run"].([]any); len(run) != 2 || run[1] != mode {
t.Errorf("%s runs the bundle's %s: %v", id, mode, byID[id]["run"])
}
}
if byID["config"]["path"] != ConfigFile {
t.Errorf("the config file is %v, the code reads %s", byID["config"]["path"], ConfigFile)
}
content, _ := byID["config"]["content"].(string)
cfg := ParseConfig(content)
if len(cfg.Problems) != 0 && !strings.Contains(strings.Join(cfg.Problems, " "), "${") {
t.Errorf("the config's template reads: %v", cfg.Problems)
}
for _, want := range []string{"server=${bound:nfs-share:from}", "at=${bound:nfs-share:at}", "secret=${dir:state}/smb-credentials.secret"} {
if !strings.Contains(content, want) {
t.Errorf("the config lacks %s", want)
}
}
if byID["bus-dir"]["path"] != BusDir || byID["bus-dir"]["owner"] != "${machine:account}" {
t.Errorf("the bundle, as the operator account, writes the bus's answer: %v", byID["bus-dir"])
}
if strings.Contains(raw, "password") {
t.Error("the manifest holds no password")
}
}