mesh/delivery merged: source.main moved to novox/mesh-host@f8acfaeed79d
mesh/delivery-group group feature/1979-zfs-owns-zfs merged: every member is merged
mesh/check being carried to 96a869063977 on f8acfaeed79d | from 0193949efe9f, mesh-catalog 117f933dacf0, mesh-delivery 7221a1e5cd7f, mesh-sdk e42b9491…
A size of a million MiB or more reached the file in exponent form and was refused. Builds: design 42 zfs-owns-zfs §3, A-24
152 lines
8.9 KiB
Go
152 lines
8.9 KiB
Go
package resolution
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// A config value reaches a resource from the assignment's layers, the node's over the mesh's (novox/hq ADR 0112,
|
|
// ADR 0155), and a value nothing set is refused by name; and a resource under `if-config` is declared only while a
|
|
// config value holds (design 42 zfs-owns-zfs §2, ADR 0679 §16 (b)). One table of the two, each row its layers, what it
|
|
// does to a resource and what comes of it.
|
|
func TestAConfigValueReachesAResourceAndDecidesWhetherItIsDeclared(t *testing.T) {
|
|
filled := func(content string) func([]mcat.Layer) (string, error) {
|
|
return func(layers []mcat.Layer) (string, error) {
|
|
file := map[string]any{"id": "env", "type": "file", "path": "/x/mail.env", "content": content}
|
|
err := settingInto(file, layers, "mail", "n", "b", "s", "l", "o")
|
|
return file["content"].(string), err
|
|
}
|
|
}
|
|
// declared is the ids of what is left of a resource under each condition, and whether the key is gone from it.
|
|
declared := func(conditions ...any) func([]mcat.Layer) (string, error) {
|
|
return func(layers []mcat.Layer) (string, error) {
|
|
kept := withoutUnconfigured([]map[string]any{
|
|
{"id": "plain", "type": "file"}, {"id": "cond", "type": "file", mcat.IfConfigField: conditions}}, layers)
|
|
var ids []string
|
|
for _, r := range kept {
|
|
if _, has := r[mcat.IfConfigField]; has {
|
|
return "", errKeyKept
|
|
}
|
|
ids = append(ids, r["id"].(string))
|
|
}
|
|
return strings.Join(ids, ","), nil
|
|
}
|
|
}
|
|
values := func(kv map[string]any) []mcat.Layer { return []mcat.Layer{{From: "the mesh", Values: kv}} }
|
|
for _, row := range []struct {
|
|
name string
|
|
layers []mcat.Layer
|
|
do func([]mcat.Layer) (string, error)
|
|
want string // what comes of it
|
|
errs []string // or what the refusal says
|
|
}{
|
|
{"the node's layer over the mesh's", []mcat.Layer{
|
|
{From: "the mesh", Values: map[string]any{"domain": "example.tld", "sitename": "Mesh", "workers": float64(4)}},
|
|
{From: "this node", Values: map[string]any{"sitename": "This one"}}},
|
|
filled("DOMAIN=${setting:domain}\nSITENAME=${setting:sitename}\nWORKERS=${setting:workers}\n"),
|
|
"DOMAIN=example.tld\nSITENAME=This one\nWORKERS=4\n", nil},
|
|
{"a value nothing set is refused, the file left as it was", values(map[string]any{"other": "x"}),
|
|
filled("DOMAIN=${setting:domain}\n"), "DOMAIN=${setting:domain}\n",
|
|
[]string{"${setting:domain}", "settings set mail", "set today: other"}},
|
|
{"config-lines: a list of two writes two lines in order", values(map[string]any{"k": []any{"a", "b"}}),
|
|
filled("h\n${config-lines:k:options zfs x=}\nt\n"), "h\noptions zfs x=a\noptions zfs x=b\nt\n", nil},
|
|
{"config-lines: mib-bytes writes a million MiB as a whole number", values(map[string]any{"k": float64(1000000)}),
|
|
filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil},
|
|
{"config-lines: mib-bytes writes a million MiB given as text as a whole number", values(map[string]any{"k": "1000000"}),
|
|
filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil},
|
|
{"config-lines: mib-bytes writes a size printed in exponent form as a whole number", values(map[string]any{"k": "1e+06"}),
|
|
filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil},
|
|
{"config-lines: mib-bytes refuses a fraction", values(map[string]any{"k": "1.5"}),
|
|
filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "${config-lines:k:zfs_arc_max=:mib-bytes}\n", []string{"mib-bytes"}},
|
|
{"config-lines: an empty list writes nothing", values(map[string]any{"k": []any{}}),
|
|
filled("h\n${config-lines:k:p=}\nt\n"), "h\nt\n", nil},
|
|
{"config-lines: an unset value writes nothing", nil, filled("h\n${config-lines:k:p=}\nt\n"), "h\nt\n", nil},
|
|
{"config-lines: a set scalar writes one line", values(map[string]any{"k": "v"}),
|
|
filled("${config-lines:k:p=}\n"), "p=v\n", nil},
|
|
{"config-lines: mib-bytes writes MiB in bytes", values(map[string]any{"k": float64(64)}),
|
|
filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=67108864\n", nil},
|
|
{"config-lines: an entry with a newline is refused, the file left as it was", values(map[string]any{"k": []any{"a\nb"}}),
|
|
filled("${config-lines:k:p=}\n"), "${config-lines:k:p=}\n", []string{"newline"}},
|
|
{"config-json: the set values only, each as its own type", values(map[string]any{"n": float64(64), "b": true, "s": "x",
|
|
"l": []any{"a"}, "o": map[string]any{"f": float64(1)}}), filled("${config-json}\n"),
|
|
"{\n \"b\": true,\n \"l\": [\n \"a\"\n ],\n \"n\": 64,\n \"o\": {\n \"f\": 1\n },\n \"s\": \"x\"\n}\n", nil},
|
|
{"config-json: a key the module does not declare is not written", values(map[string]any{"s": "x", "scrub-pool": "tank"}),
|
|
filled("${config-json}\n"), "{\n \"s\": \"x\"\n}\n", nil},
|
|
{"config-json: nothing set is an empty object, not a refusal", nil, filled("${config-json}\n"), "{}\n", nil},
|
|
{"if-config: a key never set leaves it out", nil, declared("k"), "plain", nil},
|
|
{"if-config: a key set to text declares it", values(map[string]any{"k": "x"}), declared("k"), "plain,cond", nil},
|
|
{"if-config: a key set to false, off, none, empty or an empty list leaves it out", values(map[string]any{"k": false}),
|
|
declared("k"), "plain", nil},
|
|
{"if-config: off", values(map[string]any{"k": "off"}), declared("k"), "plain", nil},
|
|
{"if-config: none", values(map[string]any{"k": "none"}), declared("k"), "plain", nil},
|
|
{"if-config: empty text", values(map[string]any{"k": ""}), declared("k"), "plain", nil},
|
|
{"if-config: an empty list", values(map[string]any{"k": []any{}}), declared("k"), "plain", nil},
|
|
{"if-config: a list of entries", values(map[string]any{"k": []any{"a"}}), declared("k"), "plain,cond", nil},
|
|
{"if-config: a value equal to the condition", values(map[string]any{"k": "on"}), declared("k=on"), "plain,cond", nil},
|
|
{"if-config: a value other than the condition", values(map[string]any{"k": "off"}), declared("k=on"), "plain", nil},
|
|
{"if-config: a list holding the entry", values(map[string]any{"k": []any{"a", "b"}}), declared("k=b"), "plain,cond", nil},
|
|
{"if-config: a list without the entry", values(map[string]any{"k": []any{"a"}}), declared("k=b"), "plain", nil},
|
|
{"if-config: any of the conditions", values(map[string]any{"j": "x"}), declared("k", "j"), "plain,cond", nil},
|
|
{"if-config: a value from a default layer", []mcat.Layer{{Default: true, Values: map[string]any{"k": "on"}}},
|
|
declared("k=on"), "plain,cond", nil},
|
|
{"if-config: a number equal to the condition", values(map[string]any{"k": float64(64)}), declared("k=64"), "plain,cond", nil},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
got, err := row.do(row.layers)
|
|
if len(row.errs) == 0 && err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(row.errs) > 0 && err == nil {
|
|
t.Fatalf("a refusal was expected, got %q", got)
|
|
}
|
|
for _, want := range row.errs {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal lacks %q: %v", want, err)
|
|
}
|
|
}
|
|
if got != row.want {
|
|
t.Fatalf("got %q, want %q", got, row.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// errKeyKept is what a row of the table above says when `if-config` reached the node-engine, which parses strictly.
|
|
var errKeyKept = errors.New("the if-config key was kept in a declared resource")
|
|
|
|
// The composition takes a resource out while its condition does not hold, and a value going from set to unset takes it
|
|
// out of the next composition, which the node-engine removes as it removes any resource that left (design 42 §2).
|
|
func TestAConfigConditionedResourceIsComposedWhileItsValueHolds(t *testing.T) {
|
|
m := mustParse(t, `{"module":"zfs","version":"1","config":{"zed":{"type":"string","default":"off"},"mods":{"type":"list"}},"resources":[
|
|
{"id":"opts","type":"file","path":"/etc/modprobe.d/zfs.conf","content":"${config-lines:mods:options zfs }\n${config-json}\n"},
|
|
{"id":"conf","type":"file","path":"/etc/zfs/mesh.conf","content":"x\n"},
|
|
{"id":"zed","type":"file","path":"/etc/zfs/zed.rc","content":"y\n","if-config":["zed=on"]}]}`)
|
|
for _, row := range []struct {
|
|
layers []mcat.Layer
|
|
zed bool
|
|
}{{nil, false}, {[]mcat.Layer{{From: "this node", Values: map[string]any{"zed": "on", "mods": []any{"a=1"}}}}, true}} {
|
|
out, err := Declaration(mcat.Resolution{Node: "workstation", Modules: []mcat.Manifest{m}},
|
|
mcat.Rendering{Settings: mcat.SettingsBy{"zfs": row.layers}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fileNamed(out, "zfs.conf") == nil {
|
|
t.Error("the unconditioned file was left out")
|
|
}
|
|
if opts := fileNamed(out, "zfs.opts"); opts == nil || (row.zed != strings.HasPrefix(fmt.Sprint(opts["content"]), "options zfs a=1\n")) {
|
|
t.Errorf("the template's lines do not follow the list: %v", opts)
|
|
}
|
|
zed := fileNamed(out, "zfs.zed")
|
|
if (zed != nil) != row.zed {
|
|
t.Errorf("with %v the conditioned file is declared: %v, want %v", row.layers, zed != nil, row.zed)
|
|
}
|
|
if _, has := zed[mcat.IfConfigField]; has {
|
|
t.Errorf("%s reached the node-engine, which parses strictly", mcat.IfConfigField)
|
|
}
|
|
}
|
|
}
|