Files
mesh-host/internal/compose/resolution/config_lines.go
T
jschoubben 7868c9f365 Fix 1979: ${config-json} writes only the keys the module declares under config
A key a layer holds that the manifest does not declare (scrub-pool during a rename) is not a config value of the module.

Builds: design 42 zfs-owns-zfs §9, A-15, A-24, A-25
2026-11-13 01:32:27 +01:00

103 lines
3.7 KiB
Go

package resolution
import (
"encoding/json"
"fmt"
"regexp"
"slices"
"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): only keys the module declares under `config` are written, and 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, declared []string) (string, error) {
if !configJSONRef.MatchString(content) {
return content, nil
}
set := map[string]any{}
for _, layer := range layers {
for k, v := range layer.Values {
if slices.Contains(declared, k) {
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
}
// declaredConfig is the keys a module's manifest declares under `config`.
func declaredConfig(m mcat.Manifest) []string {
keys := make([]string, 0, len(m.Settings))
for k := range m.Settings {
keys = append(keys, k)
}
return keys
}