149 lines
5.9 KiB
Go
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")
|
|
}
|
|
}
|