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.`, 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") }