H3 engine-config-lines, H3b engine-config-json: a template writes lines from a config value and the config as JSON
`${config-lines:<key>:<prefix>[:mib-bytes]}` in a file's content writes one line
per entry (nothing for an unset value or empty list; a newline in an entry
refuses the composition); `${config-json}` writes the module's set config values
as one JSON object. Both join the sweep's table of fillers. Their rows join the
config table test (A-22).
Builds: design 42 zfs-owns-zfs §3, §9; answers A-15, A-24, A-22
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// configLinesRef is `${config-lines:<key>:<prefix>}` and `${config-lines:<key>:<prefix>:mib-bytes}` in a file's
|
||||
// content (design 42 zfs-owns-zfs §3, ADR 0679 §16 (c)): one line per entry of a list value, `<prefix><entry>`. The
|
||||
// prefix is literal text with no `}`, `$` or newline. A placeholder that holds a whole line takes its newline with it.
|
||||
var configLinesRef = regexp.MustCompile(`\$\{config-lines:([a-z0-9][a-z0-9_.-]*):([^}$\n]*?)(:mib-bytes)?\}(\n?)`)
|
||||
|
||||
// configJSONRef is `${config-json}` in a file's content (design 42 zfs-owns-zfs §9, answer A-15): the module's set
|
||||
// config values as one JSON object.
|
||||
var configJSONRef = regexp.MustCompile(`\$\{config-json\}`)
|
||||
|
||||
// configLinesInto writes each `${config-lines:…}` of a file's content from the layers: a line `<prefix><entry>` for each
|
||||
// entry of a list, one for a set scalar, nothing for an unset value or an empty list. With `mib-bytes` an entry given in
|
||||
// MiB is written in bytes. An entry holding a newline is refused, and the content is left as it was.
|
||||
func configLinesInto(content string, layers []mcat.Layer, module string) (string, error) {
|
||||
var refused error
|
||||
filled := configLinesRef.ReplaceAllStringFunc(content, func(token string) string {
|
||||
m := configLinesRef.FindStringSubmatch(token)
|
||||
key, prefix, mib, newline := m[1], m[2], m[3] != "", m[4]
|
||||
value, set := settingValue(layers, key)
|
||||
if !set {
|
||||
return ""
|
||||
}
|
||||
entries, isList := value.([]any)
|
||||
if !isList {
|
||||
entries = []any{value}
|
||||
}
|
||||
var lines []string
|
||||
for _, e := range entries {
|
||||
text := plainly(e)
|
||||
if strings.ContainsAny(text, "\r\n") {
|
||||
refused = fmt.Errorf("%s: an entry of the config value %q holds a newline, which would begin a line "+
|
||||
"of its own in the file it is written into (design 42 §3)", module, key)
|
||||
return token
|
||||
}
|
||||
if mib {
|
||||
n, err := strconv.ParseUint(text, 10, 64)
|
||||
if err != nil || n > 1<<44 {
|
||||
refused = fmt.Errorf("%s: the config value %q holds %q, and mib-bytes writes a whole number of MiB "+
|
||||
"in bytes (design 42 §3)", module, key, text)
|
||||
return token
|
||||
}
|
||||
text = strconv.FormatUint(n<<20, 10)
|
||||
}
|
||||
lines = append(lines, prefix+text)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(lines, "\n") + newline
|
||||
})
|
||||
if refused != nil {
|
||||
return content, refused
|
||||
}
|
||||
return filled, nil
|
||||
}
|
||||
|
||||
// configJSONInto writes `${config-json}` in a file's content as the module's set config values in one JSON object
|
||||
// (design 42 zfs-owns-zfs §9, answer A-15): what no layer sets is absent, and no unset value refuses the module; a
|
||||
// list and an object are written as JSON, a number, a boolean and a text as JSON values of their own type.
|
||||
func configJSONInto(content string, layers []mcat.Layer) (string, error) {
|
||||
if !configJSONRef.MatchString(content) {
|
||||
return content, nil
|
||||
}
|
||||
set := map[string]any{}
|
||||
for _, layer := range layers {
|
||||
for k, v := range layer.Values {
|
||||
set[k] = v
|
||||
}
|
||||
}
|
||||
var out strings.Builder
|
||||
enc := json.NewEncoder(&out)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(set); err != nil {
|
||||
return content, fmt.Errorf("the config values cannot be written as JSON: %w", err)
|
||||
}
|
||||
object := strings.TrimSuffix(out.String(), "\n")
|
||||
return configJSONRef.ReplaceAllLiteralString(content, object), nil
|
||||
}
|
||||
@@ -83,6 +83,13 @@ func settingInto(resource map[string]any, layers []mcat.Layer, module string) er
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
content, err := configLinesInto(content, layers, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if content, err = configJSONInto(content, layers); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range settingsUsed(content) {
|
||||
value, set := settingValue(layers, key)
|
||||
if !set {
|
||||
|
||||
@@ -2,6 +2,7 @@ package resolution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -51,6 +52,21 @@ func TestAConfigValueReachesAResourceAndDecidesWhetherItIsDeclared(t *testing.T)
|
||||
{"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: 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: 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}),
|
||||
@@ -95,13 +111,14 @@ 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"}},"resources":[
|
||||
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"}}}, true}} {
|
||||
}{{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 {
|
||||
@@ -110,6 +127,9 @@ func TestAConfigConditionedResourceIsComposedWhileItsValueHolds(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -25,6 +25,10 @@ type filler struct {
|
||||
// or the sweep refuses the placeholder it would have filled.
|
||||
var fillers = []filler{
|
||||
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}, "directory": {"owner"}, "container": {"groups"}}},
|
||||
// A file's content only: lines from a config value's entries, and the module's config as one JSON object
|
||||
// (design 42 zfs-owns-zfs §3, §9; config_lines.go).
|
||||
{namespace: "config-lines", patterns: []*regexp.Regexp{configLinesRef}, fields: map[string][]string{"file": {"content"}}},
|
||||
{namespace: "config-json", patterns: []*regexp.Regexp{configJSONRef}, fields: map[string][]string{"file": {"content"}}},
|
||||
{namespace: "dir", patterns: []*regexp.Regexp{dirRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
||||
|
||||
Reference in New Issue
Block a user