Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/manifest_test.go
T

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")
}