Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/manifest_test.go
T
jschoubben 35ff67bda0
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00

187 lines
6.5 KiB
Go

package main
import (
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"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"`
Config map[string]map[string]any `json:"config"`
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)
}
}
nfsConf := ""
for _, r := range m.Resources {
if r["id"] == "nfs-conf" {
nfsConf = templateText(t, r)
}
}
if !strings.Contains(nfsConf, "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 config value, required and with no default (ADR 0348 §4), 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 s := m.Config["shares"]; s["required"] != true || s["default"] != nil || s["type"] != "string" {
t.Errorf("the shares are the operator's value: required, with no default (ADR 0348 §4): %v", s)
}
var config string
for _, r := range m.Resources {
if r["id"] == "config" {
config = templateText(t, r)
}
}
if !strings.Contains(config, "shares=${config: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"`
Config map[string]map[string]any `json:"config"`
}
if err := json.Unmarshal([]byte(raw), &full); err != nil {
t.Fatal(err)
}
if len(full.Reads) != 3 || full.Reads[0] != health.ReadingsRead || full.Reads[1] != delivery.SeatHoldersRead || full.Reads[2] != 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.Config["grants"]; g["type"] != "string" || g["default"] != "none" || g["trusted"] != true {
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 = templateText(t, r)
}
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=${config: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")
}