Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -22,8 +22,8 @@ type manifest struct {
|
||||
Protocol string `json:"protocol"`
|
||||
From string `json:"from"`
|
||||
} `json:"listens"`
|
||||
Settings map[string]any `json:"settings"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Config map[string]map[string]any `json:"config"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
}
|
||||
|
||||
func readManifest(t *testing.T) (manifest, string) {
|
||||
@@ -83,28 +83,34 @@ func TestItListensOnTwoThousandFortyNineFromTheMeshOnlyAndProvidesTheShare(t *te
|
||||
t.Errorf("the module opens %s", port)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(raw, "vers3=n") {
|
||||
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 setting, and the range the mesh's.
|
||||
// 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 _, declared := m.Settings["shares"]; declared {
|
||||
t.Errorf("the shares are the operator's value and have no default (ADR 0262)")
|
||||
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, _ = r["content"].(string)
|
||||
config = templateText(t, r)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(config, "shares=${setting:shares}") || !strings.Contains(config, "range=${machine:mesh-range}") {
|
||||
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).
|
||||
@@ -124,9 +130,9 @@ func TestTheDefinitionNamesNoFolderNoMachineAndGivesTheSharesNoDefault(t *testin
|
||||
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"`
|
||||
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)
|
||||
@@ -138,7 +144,7 @@ func TestTheManifestCarriesGrantsTheRosterTheWantsAndTheRemovalOrder(t *testing.
|
||||
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" {
|
||||
if g := full.Config["grants"]; g["type"] != "string" || g["default"] != "none" || g["trusted"] != true {
|
||||
t.Errorf("grants: %v", g)
|
||||
}
|
||||
order := map[string]int{}
|
||||
@@ -146,13 +152,13 @@ func TestTheManifestCarriesGrantsTheRosterTheWantsAndTheRemovalOrder(t *testing.
|
||||
for i, r := range m.Resources {
|
||||
order[r["id"].(string)] = i
|
||||
if r["id"] == "config" {
|
||||
config, _ = r["content"].(string)
|
||||
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=${setting:grants}") {
|
||||
if !strings.Contains(config, "grants=${config:grants}") {
|
||||
t.Errorf("the config lacks the grants: %q", config)
|
||||
}
|
||||
if _, ok := order["wants-dir"]; !ok {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The module's directory, where its manifest and its files/ are.
|
||||
const moduleDir = "../.."
|
||||
|
||||
// templateText is the text a file resource places on the machine: its template's, in the module's files/
|
||||
// (novox/hq ADR 0349 §2). A resource that names no template, or a template that is not there, fails the test.
|
||||
func templateText(t *testing.T, r map[string]any) string {
|
||||
t.Helper()
|
||||
name, _ := r["template"].(string)
|
||||
if name == "" {
|
||||
t.Fatalf("resource %v names no template", r["id"])
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(moduleDir, filepath.FromSlash(name)))
|
||||
if err != nil {
|
||||
t.Fatalf("resource %v names %s: %v", r["id"], name, err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// No file resource carries its text inline: each names its template in files/, and the build packs files/
|
||||
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
||||
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Build struct {
|
||||
Artifacts []map[string]any `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
||||
}
|
||||
if r["type"] == "file" {
|
||||
templateText(t, r)
|
||||
}
|
||||
}
|
||||
packs := 0
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a["kind"] == "archive" && a["from"] == "files" {
|
||||
packs++
|
||||
if a["name"] != "files" {
|
||||
t.Errorf("the templates archive is named %v, not files", a["name"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if packs != 1 {
|
||||
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Written by the mesh (module nfs-server, novox/hq ADR 0263). Replaced on every push; a drop-in of
|
||||
# the operator's that sorts after this one overrides it, and is theirs.
|
||||
#
|
||||
# NFS version 4 only: a client needs port 2049 and nothing else, so the module opens nothing more
|
||||
# than that, to the private network. Version 3 needs rpcbind and mountd, on ports the mesh does not open.
|
||||
[nfsd]
|
||||
vers2=n
|
||||
vers3=n
|
||||
vers4=y
|
||||
vers4.0=n
|
||||
vers4.1=y
|
||||
vers4.2=y
|
||||
@@ -0,0 +1,10 @@
|
||||
# Written by the mesh (module nfs-server, novox/hq ADR 0263) from this machine's assignment.
|
||||
# Replaced on every push; change the `shares` setting, never this file.
|
||||
#
|
||||
# The shares: name=folder, or name=folder:ro, one share per folder. The grants: share=node:rw|ro[,…],
|
||||
# which nodes may mount each and how. The module's process exports a share to a node's private address
|
||||
# only when it is granted here and that node's mounts module wants it, every client mapped to the
|
||||
# folder's owner, and only to addresses inside the range below.
|
||||
shares=${config:shares}
|
||||
grants=${config:grants}
|
||||
range=${machine:mesh-range}
|
||||
@@ -51,13 +51,20 @@
|
||||
"seat:mesh-controller.seats",
|
||||
"seat:mesh-controller.data"
|
||||
],
|
||||
"settings": {
|
||||
"grants": {
|
||||
"kind": "preference",
|
||||
"default": "none",
|
||||
"why": "which nodes may mount which share, and how, is this machine's to say (hq ADR 0263, review of mesh-catalog #136): share=node:rw|ro[,node:rw|ro], entries separated by spaces; none exports to no node, whatever a node wants"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"shares": {
|
||||
"type": "string",
|
||||
"description": "The folders this machine exports, as space-separated name=path; the operator's, with no default (novox/hq ADR 0263).",
|
||||
"trusted": true,
|
||||
"required": true
|
||||
},
|
||||
"grants": {
|
||||
"type": "string",
|
||||
"description": "Which nodes may mount which share, and how: share=node:rw|ro[,node:rw|ro], entries separated by spaces, or none to export to no node whatever a node wants (novox/hq ADR 0263).",
|
||||
"trusted": true,
|
||||
"default": "none"
|
||||
}
|
||||
},
|
||||
"facts": {
|
||||
"machines": {
|
||||
"path": "/etc/nfs-server/machines",
|
||||
@@ -88,7 +95,7 @@
|
||||
"type": "file",
|
||||
"path": "/etc/nfs.conf.d/50-mesh.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Written by the mesh (module nfs-server, novox/hq ADR 0263). Replaced on every push; a drop-in of\n# the operator's that sorts after this one overrides it, and is theirs.\n#\n# NFS version 4 only: a client needs port 2049 and nothing else, so the module opens nothing more\n# than that, to the private network. Version 3 needs rpcbind and mountd, on ports the mesh does not open.\n[nfsd]\nvers2=n\nvers3=n\nvers4=y\nvers4.0=n\nvers4.1=y\nvers4.2=y\n"
|
||||
"template": "files/50-mesh.conf"
|
||||
},
|
||||
{
|
||||
"id": "run-dir",
|
||||
@@ -154,7 +161,7 @@
|
||||
"type": "file",
|
||||
"path": "/etc/nfs-server/shares.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Written by the mesh (module nfs-server, novox/hq ADR 0263) from this machine's assignment.\n# Replaced on every push; change the `shares` setting, never this file.\n#\n# The shares: name=folder, or name=folder:ro, one share per folder. The grants: share=node:rw|ro[,…],\n# which nodes may mount each and how. The module's process exports a share to a node's private address\n# only when it is granted here and that node's mounts module wants it, every client mapped to the\n# folder's owner, and only to addresses inside the range below.\nshares=${setting:shares}\ngrants=${setting:grants}\nrange=${machine:mesh-range}\n"
|
||||
"template": "files/shares.conf"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -169,6 +176,11 @@
|
||||
"loads": [
|
||||
"nfs-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user