179 lines
6.1 KiB
Go
179 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type manifest struct {
|
|
Tools []string `json:"tools"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Provides []map[string]any `json:"provides"`
|
|
Listens []struct {
|
|
Port int `json:"port"`
|
|
Protocol string `json:"protocol"`
|
|
From string `json:"from"`
|
|
} `json:"listens"`
|
|
Settings map[string]any `json:"settings"`
|
|
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 seat's verbs are served as `node-nfs-server.<verb>`, the health tool is listed, and every tool says
|
|
// whether it reads or acts.
|
|
func TestTheSeatsVerbsAreAllServedAndTheHealthToolListed(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || m.Claims[0].Scope != "node" {
|
|
t.Fatalf("claims: %+v", m.Claims)
|
|
}
|
|
var verbs, tools []string
|
|
for _, tl := range Tools(newFake(t).machine()) {
|
|
if name, ok := strings.CutPrefix(tl.Name, Seat+"."); ok {
|
|
verbs = append(verbs, name)
|
|
} else {
|
|
tools = append(tools, tl.Name)
|
|
}
|
|
if !strings.HasSuffix(tl.Description, "(r)") && !strings.HasSuffix(tl.Description, "(a)") {
|
|
t.Errorf("%s does not say whether it reads or acts", tl.Name)
|
|
}
|
|
}
|
|
sort.Strings(verbs)
|
|
serves := append([]string(nil), m.Claims[0].Serves...)
|
|
sort.Strings(serves)
|
|
if strings.Join(verbs, ",") != strings.Join(serves, ",") {
|
|
t.Errorf("served %v, the claim lists %v", verbs, serves)
|
|
}
|
|
if strings.Join(tools, ",") != strings.Join(m.Tools, ",") {
|
|
t.Errorf("tools %v, listed %v", tools, m.Tools)
|
|
}
|
|
}
|
|
|
|
// Port 2049 to the mesh's machines and nothing else; the provision is the mesh's, keeping no name of its
|
|
// consumers.
|
|
func TestItListensOnTwoThousandFortyNineFromTheMeshOnlyAndProvidesTheShare(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
if len(m.Listens) != 1 || m.Listens[0].Port != 2049 || m.Listens[0].Protocol != "tcp" || m.Listens[0].From != "mesh" {
|
|
t.Errorf("listens: %+v", m.Listens)
|
|
}
|
|
if len(m.Provides) != 1 || m.Provides[0]["name"] != "nfs-share" || m.Provides[0]["scope"] != "mesh" || m.Provides[0]["identity"] != false {
|
|
t.Errorf("provides: %+v", m.Provides)
|
|
}
|
|
for _, port := range []string{"111", "20048", "\"port\": 445"} {
|
|
if strings.Contains(raw, `"port": `+port) {
|
|
t.Errorf("the module opens %s", port)
|
|
}
|
|
}
|
|
if !strings.Contains(raw, "vers3=n") {
|
|
t.Errorf("NFS version 3 is not switched off, so 2049 alone would not do")
|
|
}
|
|
}
|
|
|
|
// The definition names no folder, no machine and no person (novox/hq ADR 0112): the shares are the
|
|
// assignment's setting, and the range the mesh's.
|
|
func TestTheDefinitionNamesNoFolderNoMachineAndGivesTheSharesNoDefault(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/|/storage|/srv/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" {
|
|
t.Errorf("the manifest says %q", hit)
|
|
}
|
|
if _, declared := m.Settings["shares"]; declared {
|
|
t.Errorf("the shares are the operator's value and have no default (ADR 0262)")
|
|
}
|
|
var config string
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "config" {
|
|
config, _ = r["content"].(string)
|
|
}
|
|
}
|
|
if !strings.Contains(config, "shares=${setting:shares}") || !strings.Contains(config, "range=${machine:mesh-range}") {
|
|
t.Errorf("the config file: %q", config)
|
|
}
|
|
// Every long-running resource says how it is ready (ADR 0240 rule 8).
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "service" || r["type"] == "process" {
|
|
if r["health"] == nil {
|
|
t.Errorf("%v has no health", r["id"])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Review points 5 and 7: the module reads the nodes' wants, renders the mesh's roster, has grants that
|
|
// default to none, and its process is declared before its config, so the node-engine — removing a
|
|
// module's resources in reverse order — takes the config away before it stops the process, which then
|
|
// lets the shares go.
|
|
func TestTheManifestCarriesGrantsTheRosterTheWantsAndTheRemovalOrder(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
var full struct {
|
|
Reads []string `json:"reads"`
|
|
Facts map[string]map[string]string `json:"facts"`
|
|
Settings map[string]map[string]any `json:"settings"`
|
|
}
|
|
if err := json.Unmarshal([]byte(raw), &full); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(full.Reads) != 1 || full.Reads[0] != MountsState {
|
|
t.Errorf("reads: %v", full.Reads)
|
|
}
|
|
roster := full.Facts["machines"]
|
|
if roster["path"] != RosterFile || !strings.Contains(roster["template"], "{{range .Machines}}{{.Name}} {{.Address}}") {
|
|
t.Errorf("the roster fact: %v", roster)
|
|
}
|
|
if g := full.Settings["grants"]; g["kind"] != "preference" || g["default"] != "none" {
|
|
t.Errorf("grants: %v", g)
|
|
}
|
|
order := map[string]int{}
|
|
var config string
|
|
for i, r := range m.Resources {
|
|
order[r["id"].(string)] = i
|
|
if r["id"] == "config" {
|
|
config, _ = r["content"].(string)
|
|
}
|
|
if r["path"] == WantsDir && r["owner"] != "${machine:account}" {
|
|
t.Errorf("the tools, as the operator account, cannot write the wants: %v", r)
|
|
}
|
|
}
|
|
if !strings.Contains(config, "grants=${setting:grants}") {
|
|
t.Errorf("the config lacks the grants: %q", config)
|
|
}
|
|
if _, ok := order["wants-dir"]; !ok {
|
|
t.Errorf("no directory for the wants")
|
|
}
|
|
if !(order["exports"] < order["config"] && order["exports"] < order["config-dir"]) {
|
|
t.Errorf("the process must be declared before its config, so unassignment removes the config first: %v", order)
|
|
}
|
|
}
|
|
|
|
// Stopped asks whether the process's own unit is disabled; that unit is the manifest's process name.
|
|
func TestTheProcessUnitIsTheManifestsProcess(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" && r["id"] == "exports" {
|
|
if got := r["name"].(string) + ".service"; got != ProcessUnit {
|
|
t.Errorf("the exports process runs as %s, and Stopped asks about %s", got, ProcessUnit)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("the manifest has no exports process")
|
|
}
|