V2 judge-config-lines: the form ${config-lines:<key>:<prefix>[:mib-bytes]} is judged and places its key
Builds: design 42 zfs-owns-zfs §3; answers A-4, A-5, A-24
This commit is contained in:
@@ -872,6 +872,9 @@ func configPlaced(m catalogue.Manifest) map[string]bool {
|
||||
placed[k] = true
|
||||
}
|
||||
}
|
||||
for _, u := range configLinesUses(m) {
|
||||
placed[u.key] = true
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
@@ -2835,6 +2838,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
|
||||
}
|
||||
// And the config values its resources are declared under (design 42 zfs-owns-zfs §2).
|
||||
problems = append(problems, IfConfigProblems(m)...)
|
||||
problems = append(problems, ConfigLinesProblems(m)...)
|
||||
// And what it requires optionally, and the files it delivers only while bound (novox/hq ADR 0330).
|
||||
problems = append(problems, OptionalProblems(m)...)
|
||||
// And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326).
|
||||
@@ -7306,6 +7310,10 @@ var fillers = []filler{
|
||||
{namespace: "port", patterns: []*regexp.Regexp{ofPort}, fields: map[string][]string{"file": {"content"}, "container": {"env"}, "process": {"env"}}},
|
||||
{namespace: "seat", patterns: []*regexp.Regexp{ofSeat, ofSeatReach}, fields: map[string][]string{"file": {"content"}, "container": {"env"}, "process": {"env"}}},
|
||||
{namespace: "machine", patterns: []*regexp.Regexp{ofMachine}, fields: map[string][]string{"*": {"path", "owner", "content", "name", "user", "root", "home"}}},
|
||||
// The module's own config values written into a file (design 42 zfs-owns-zfs §3, §9), filled by the node-engine
|
||||
// and judged by ConfigLinesProblems.
|
||||
{namespace: "config-lines", patterns: []*regexp.Regexp{ofConfigLines}, fields: map[string][]string{"file": {"content"}}},
|
||||
{namespace: "config-json", patterns: []*regexp.Regexp{ofConfigJSON}, fields: map[string][]string{"file": {"content"}}},
|
||||
// Placed in a file's content by their holders, and judged there by their own rules
|
||||
// (placeholderProblems, seatPlaceholderProblems).
|
||||
{namespace: "environment", patterns: []*regexp.Regexp{ofEnvironment}, fields: map[string][]string{"*": {"content"}}},
|
||||
@@ -7348,6 +7356,74 @@ func whereFilled(f filler) string {
|
||||
var ofKnownNamespace = regexp.MustCompile(`(?im)\$\{(?:` + strings.Join(namespacesOf(fillers), "|") +
|
||||
`):(?:[^-=+?}\n][^}\n]*\}?|\}|$)`)
|
||||
|
||||
// ofConfigLines is a `${config-lines:<key>:<prefix>}` or `${config-lines:<key>:<prefix>:mib-bytes}` whose prefix is
|
||||
// literal text with no `}`, `$`, newline or `:` (design 42 zfs-owns-zfs §3, answers A-4, A-5).
|
||||
var ofConfigLines = regexp.MustCompile(`\$\{config-lines:[a-z0-9][a-z0-9_.-]*:[^}$\n:]*(?::mib-bytes)?\}`)
|
||||
|
||||
// ofConfigJSON is the module's config values as one JSON object (design 42 zfs-owns-zfs §9, answer A-15).
|
||||
var ofConfigJSON = regexp.MustCompile(`\$\{config-json\}`)
|
||||
|
||||
var configLinesToken = regexp.MustCompile(`\$\{config-lines:([^}]*)\}`)
|
||||
|
||||
// configLinesUse is one `${config-lines:…}` of a file's content, split into its parts.
|
||||
type configLinesUse struct {
|
||||
resource any
|
||||
key string
|
||||
prefix string
|
||||
extra []string // the parts after the prefix
|
||||
parts int
|
||||
}
|
||||
|
||||
// configLinesUses is every `${config-lines:…}` in the content of the module's files, to the first `}`.
|
||||
func configLinesUses(m catalogue.Manifest) []configLinesUse {
|
||||
var out []configLinesUse
|
||||
for _, r := range m.Resources {
|
||||
content, _ := r["content"].(string)
|
||||
if fmt.Sprint(r["type"]) != "file" {
|
||||
continue
|
||||
}
|
||||
for _, t := range configLinesToken.FindAllStringSubmatch(content, -1) {
|
||||
parts := strings.Split(t[1], ":")
|
||||
u := configLinesUse{resource: r["id"], key: parts[0], parts: len(parts)}
|
||||
if len(parts) > 1 {
|
||||
u.prefix = parts[1]
|
||||
u.extra = parts[2:]
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ConfigLinesProblems are the `${config-lines:<key>:<prefix>[:mib-bytes]}` a file says wrongly: a prefix holding `$`, a
|
||||
// newline or `:`, a fourth part other than `mib-bytes`, a key the module does not declare, and `mib-bytes` on a value
|
||||
// that is not a number (design 42 zfs-owns-zfs §3; answers A-4, A-5). A `}` ends the token where it stands.
|
||||
func ConfigLinesProblems(m catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, u := range configLinesUses(m) {
|
||||
say := func(format string, args ...any) {
|
||||
out = append(out, fmt.Sprintf("%s's resource %v says ${config-lines:%s…}: ", m.Module, u.resource, u.key)+
|
||||
fmt.Sprintf(format, args...)+" (design 42 zfs-owns-zfs §3)")
|
||||
}
|
||||
d, declared := m.Settings[u.key]
|
||||
if !declared {
|
||||
say("the module declares no value %q under %s", u.key, catalogue.ConfigField)
|
||||
}
|
||||
switch {
|
||||
case u.parts < 2:
|
||||
say("it has no prefix: ${config-lines:<key>:<prefix>}")
|
||||
continue
|
||||
case strings.ContainsAny(u.prefix, "$\n"):
|
||||
say("the prefix is literal text with no }, $ or newline")
|
||||
case len(u.extra) > 1 || len(u.extra) == 1 && u.extra[0] != "mib-bytes":
|
||||
say("the prefix holds no ':', and the only fourth part is mib-bytes")
|
||||
case len(u.extra) == 1 && declared && d.Type != catalogue.ConfigNumber:
|
||||
say("mib-bytes is for a number, and %q is a %s", u.key, d.Type)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var namespaceShaped = regexp.MustCompile(`\$\{[a-z][a-z0-9_-]*:[A-Za-z0-9][^\s{}$]*\}`)
|
||||
|
||||
func namespacesOf(fs []filler) []string {
|
||||
|
||||
@@ -3563,6 +3563,37 @@ func caseIfConfigNamesADeclaredValueAndPlacesIt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Design 42 zfs-owns-zfs §3, V2/AC1 to AC3 (A-4, A-5, A-24): `${config-lines:<key>:<prefix>[:mib-bytes]}`.
|
||||
func caseConfigLinesNamesADeclaredValueAndAPlainPrefix(t *testing.T) {
|
||||
withLines := func(content string) map[string]any {
|
||||
m := inConfigForm()
|
||||
m["config"].(map[string]any)["opts"] = map[string]any{"type": "list", "trusted": true, "description": "options, one per line", "default": []any{}}
|
||||
m["resources"] = append(m["resources"].([]any), map[string]any{"id": "options", "type": "file", "path": "/x/o.conf", "content": content})
|
||||
return m
|
||||
}
|
||||
// A prefix holding $, a newline or ':' is refused, naming the key; so is a fourth part but mib-bytes.
|
||||
for _, c := range []string{"${config-lines:opts:a$b}", "${config-lines:opts:a\nb}", "${config-lines:opts:a:b}",
|
||||
"${config-lines:opts:a:mib-bytes:x}", "${config-lines:opts}"} {
|
||||
refused(t, withLines(c), "${config-lines:opts", "design 42 zfs-owns-zfs §3")
|
||||
}
|
||||
// Accepted, and the key is placed (opts is placed by nothing else).
|
||||
for _, c := range []string{"${config-lines:opts:options zfs x=}", "${config-lines:opts:}"} {
|
||||
if _, err := parseRaw(t, withLines(c)); err != nil {
|
||||
t.Fatalf("%s: %v", c, err)
|
||||
}
|
||||
}
|
||||
if _, err := parseRaw(t, withLines("# none\n")); err == nil || !strings.Contains(err.Error(), "placed nowhere") {
|
||||
t.Fatalf("a list value placed nowhere was accepted: %v", err)
|
||||
}
|
||||
// mib-bytes is for a number; a key not declared is refused.
|
||||
m := withLines("${config-lines:opts:o=}\n${config-lines:width:w=:mib-bytes}")
|
||||
if _, err := parseRaw(t, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refused(t, withLines("${config-lines:opts:o=:mib-bytes}"), "mib-bytes is for a number", `"opts" is a list`)
|
||||
refused(t, withLines("${config-lines:nowhere:o=}"), `no value "nowhere"`)
|
||||
}
|
||||
|
||||
// Consumer data said to live in a provision the module requires is protected only as well as that
|
||||
// provision's provider protects its consumers' data.
|
||||
func caseConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) {
|
||||
@@ -6158,6 +6189,7 @@ func TestRulesConfig(t *testing.T) {
|
||||
t.Run("ConfigRefusesAValueWithoutTypeOrDescription", caseConfigRefusesAValueWithoutTypeOrDescription)
|
||||
t.Run("ConfigStillRefusesAValueNoItemPlaces", caseConfigStillRefusesAValueNoItemPlaces)
|
||||
t.Run("IfConfigNamesADeclaredValueAndPlacesIt", caseIfConfigNamesADeclaredValueAndPlacesIt)
|
||||
t.Run("ConfigLinesNamesADeclaredValueAndAPlainPrefix", caseConfigLinesNamesADeclaredValueAndAPlainPrefix)
|
||||
}
|
||||
|
||||
// TestRulesClaims runs the tests of one kind as subtests, each by its former name.
|
||||
|
||||
Reference in New Issue
Block a user