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:
@@ -16,7 +16,7 @@ type manifest struct {
|
||||
Invokes []string `json:"invokes"`
|
||||
Claims []map[string]any `json:"claims"`
|
||||
State []map[string]any `json:"state"`
|
||||
Settings map[string]map[string]any `json:"settings"`
|
||||
Config map[string]map[string]any `json:"config"`
|
||||
OwnSecrets map[string]map[string]string `json:"own-secrets"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
}
|
||||
@@ -31,7 +31,15 @@ func readManifest(t *testing.T) (manifest, string) {
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m, string(raw)
|
||||
// And the text of each file it places, kept in files/ as its template (novox/hq ADR 0349 §2): what the
|
||||
// machine is given is judged as a whole.
|
||||
text := string(raw)
|
||||
for _, r := range m.Resources {
|
||||
if _, has := r["template"]; has {
|
||||
text += templateText(t, r)
|
||||
}
|
||||
}
|
||||
return m, text
|
||||
}
|
||||
|
||||
// The tools the bundle serves are the manifest's tools and the seat's verbs, each saying whether it reads
|
||||
@@ -91,13 +99,14 @@ func TestTheManifestNamesWhatTheBundleServesAndWhatItNeeds(t *testing.T) {
|
||||
if got := PasswordFile("${dir:state}", "games"); got != strings.Replace(sec["path"], "*", "games", 1) {
|
||||
t.Errorf("the process reads %s, the mesh places %s", got, sec["path"])
|
||||
}
|
||||
if _, has := m.Settings["smb-users"]; has || len(m.Settings) != 2 {
|
||||
t.Errorf("an SMB source's username is in its entry of sources, not a setting of its own: %v", m.Settings)
|
||||
if _, has := m.Config["smb-users"]; has || len(m.Config) != 2 {
|
||||
t.Errorf("an SMB source's username is in its entry of sources, not a config value of its own: %v", m.Config)
|
||||
}
|
||||
for _, key := range []string{"shares", "sources"} {
|
||||
s := m.Settings[key]
|
||||
if s["kind"] != "preference" || s["default"] != None || s["why"] == "" || !strings.Contains(raw, "${setting:"+key+"}") {
|
||||
t.Errorf("setting %s: %v", key, s)
|
||||
s := m.Config[key]
|
||||
if s["type"] != "string" || s["default"] != None || s["description"] == "" || s["trusted"] != true ||
|
||||
!strings.Contains(raw, "${config:"+key+"}") {
|
||||
t.Errorf("config value %s: %v", key, s)
|
||||
}
|
||||
}
|
||||
// A machine's or a person's name, as a word: "novox/hq" names the record, not the machine.
|
||||
@@ -137,7 +146,7 @@ func TestTheProcessesAreRootAndJudgedByTheirUnitAndTheHealthTool(t *testing.T) {
|
||||
if byID["config"]["path"] != ConfigFile {
|
||||
t.Errorf("the config file is %v, the code reads %s", byID["config"]["path"], ConfigFile)
|
||||
}
|
||||
content, _ := byID["config"]["content"].(string)
|
||||
content := templateText(t, byID["config"])
|
||||
cfg := ParseConfig(content)
|
||||
if len(cfg.Problems) != 0 && !strings.Contains(strings.Join(cfg.Problems, " "), "${") {
|
||||
t.Errorf("the config's template reads: %v", cfg.Problems)
|
||||
|
||||
@@ -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,11 @@
|
||||
# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings
|
||||
# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this.
|
||||
# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.
|
||||
shares=${config:shares}
|
||||
sources=${config:sources}
|
||||
server=${bound:nfs-share:from}
|
||||
at=${bound:nfs-share:at}
|
||||
account=${machine:account}
|
||||
node=${machine:name}
|
||||
passwords=${dir:state}
|
||||
state=${dir:state}
|
||||
+19
-12
@@ -38,18 +38,20 @@
|
||||
"per-machine": true
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"shares": {
|
||||
"kind": "preference",
|
||||
"default": "none",
|
||||
"why": "a machine mounts no share of another until its assignment names one: space-separated name=mountpoint, each optionally :ro (hq ADR 0263 rule 5)"
|
||||
"config": {
|
||||
"shares": {
|
||||
"type": "string",
|
||||
"description": "The shares of another machine this machine mounts: space-separated name=mountpoint, each optionally :ro, or none (novox/hq ADR 0263 rule 5).",
|
||||
"trusted": true,
|
||||
"default": "none"
|
||||
},
|
||||
"sources": {
|
||||
"type": "string",
|
||||
"description": "The occasional sources this machine mounts: space-separated name=smb://[<user>@]<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro, or none (novox/hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password-<name> (novox/hq ADR 0283).",
|
||||
"trusted": true,
|
||||
"default": "none"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"kind": "preference",
|
||||
"default": "none",
|
||||
"why": "a machine has no occasional source until its assignment names one: space-separated name=smb://[<user>@]<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro (hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password-<name> (hq ADR 0283)"
|
||||
}
|
||||
},
|
||||
"own-secrets": {
|
||||
"smb-password-*": {
|
||||
"path": "${dir:state}/smb-password-*.secret",
|
||||
@@ -84,7 +86,7 @@
|
||||
"type": "file",
|
||||
"path": "/etc/mesh-mounts/mounts.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n"
|
||||
"template": "files/mounts.conf"
|
||||
},
|
||||
{
|
||||
"id": "bus-dir-parent",
|
||||
@@ -143,6 +145,11 @@
|
||||
"loads": [
|
||||
"mounts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user