V1 judge-if-config: a resource's if-config names a declared config value and places it

Builds: design 42 zfs-owns-zfs §2; answer A-24
This commit is contained in:
2026-11-13 01:27:58 +01:00
parent 7221a1e5cd
commit 27c3b9a860
2 changed files with 86 additions and 2 deletions
+54 -2
View File
@@ -813,10 +813,60 @@ func TypedSettingProblems(m catalogue.Manifest) []string {
return problems return problems
} }
// configPlaced is every value a file, a unit, a directory's owner, a contribution or a served fact asks for, and // ifConfigKeys is the keys a resource's `if-config` names, each `<key>` or `<key>=<value>` by its key, and what is
// every value a mergeable file takes. // not a list of strings (design 42 zfs-owns-zfs §2).
func ifConfigKeys(r map[string]any) (keys []string, malformed bool) {
raw, said := r[catalogue.IfConfigField]
if !said {
return nil, false
}
list, ok := raw.([]any)
if !ok {
return nil, true
}
for _, e := range list {
s, ok := e.(string)
if !ok {
malformed = true
continue
}
key, _, _ := strings.Cut(s, "=")
keys = append(keys, key)
}
return keys, malformed
}
// IfConfigProblems are the resources whose `if-config` names a key the module does not declare under `config`, on a
// resource of any type (design 42 zfs-owns-zfs §2, answer A-24).
func IfConfigProblems(m catalogue.Manifest) []string {
var out []string
for _, r := range m.Resources {
keys, malformed := ifConfigKeys(r)
if malformed {
out = append(out, fmt.Sprintf("%s's resource %v says %s as something but a list of <key> and <key>=<value> "+
"strings", m.Module, r["id"], catalogue.IfConfigField))
}
for _, key := range keys {
if _, declared := m.Settings[key]; !declared {
out = append(out, fmt.Sprintf("%s's resource %v says %s with %q, and the module declares no such value "+
"under %s: declare it, or name a declared one (design 42 zfs-owns-zfs §2)", m.Module, r["id"],
catalogue.IfConfigField, key, catalogue.ConfigField))
}
}
}
return out
}
// configPlaced is every value a file, a unit, a directory's owner, a contribution or a served fact asks for, every
// value a mergeable file takes, and every value a resource is declared under (`if-config`).
func configPlaced(m catalogue.Manifest) map[string]bool { func configPlaced(m catalogue.Manifest) map[string]bool {
placed := settingKeysUsedBy(m) placed := settingKeysUsedBy(m)
for _, r := range m.Resources {
keys, _ := ifConfigKeys(r)
for _, k := range keys {
placed[k] = true
}
}
for _, r := range m.Resources { for _, r := range m.Resources {
for _, k := range takenKeyList(r) { for _, k := range takenKeyList(r) {
placed[k] = true placed[k] = true
@@ -2783,6 +2833,8 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
problems = append(problems, SettingProblems(m)...) problems = append(problems, SettingProblems(m)...)
problems = append(problems, TypedSettingProblems(m)...) problems = append(problems, TypedSettingProblems(m)...)
} }
// And the config values its resources are declared under (design 42 zfs-owns-zfs §2).
problems = append(problems, IfConfigProblems(m)...)
// And what it requires optionally, and the files it delivers only while bound (novox/hq ADR 0330). // And what it requires optionally, and the files it delivers only while bound (novox/hq ADR 0330).
problems = append(problems, OptionalProblems(m)...) problems = append(problems, OptionalProblems(m)...)
// And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326). // And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326).
@@ -3532,6 +3532,37 @@ func caseConfigStillRefusesAValueNoItemPlaces(t *testing.T) {
refused(t, m, `"k"`, "placed nowhere") refused(t, m, `"k"`, "placed nowhere")
} }
// Design 42 zfs-owns-zfs §2, V1/AC1 to AC3 (A-24): `if-config` names a declared value, on a resource of any type, and
// places it.
func caseIfConfigNamesADeclaredValueAndPlacesIt(t *testing.T) {
m := inConfigForm()
resourceOf(m, "notifier")["if-config"] = []any{"nowhere"}
refused(t, m, "notifier", `resource notifier`, "if-config", `"nowhere"`)
m = inConfigForm()
resourceOf(m, "rules")["if-config"] = []any{"theme=dark", "nowhere=on"}
refused(t, m, `"nowhere"`)
m = inConfigForm()
resourceOf(m, "rules")["if-config"] = "theme"
refused(t, m, "if-config", "list")
// <key> and <key>=<value> of a declared key, on a file, a service and a directory.
m = inConfigForm()
resourceOf(m, "configuration")["if-config"] = []any{"width", "theme=dark"}
resourceOf(m, "notifier")["if-config"] = []any{"position=top-left"}
m["resources"] = append(m["resources"].([]any), map[string]any{"id": "cache", "type": "directory", "path": "/x/c",
"mode": "0755", "if-config": []any{"theme"}})
if _, err := parseRaw(t, m); err != nil {
t.Fatal(err)
}
// A value placed only by if-config is not placed nowhere.
m = inConfigForm()
m["config"].(map[string]any)["zed"] = map[string]any{"type": "string", "description": "whether the daemon runs", "default": "off"}
refused(t, m, `"zed"`, "placed nowhere")
resourceOf(m, "notifier")["if-config"] = []any{"zed=on"}
if _, err := parseRaw(t, m); err != nil {
t.Fatal(err)
}
}
// Consumer data said to live in a provision the module requires is protected only as well as that // 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. // provision's provider protects its consumers' data.
func caseConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) { func caseConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) {
@@ -6126,6 +6157,7 @@ func TestRulesConfig(t *testing.T) {
t.Run("ConfigRefusesAValueWithNeitherDefaultNorRequired", caseConfigRefusesAValueWithNeitherDefaultNorRequired) t.Run("ConfigRefusesAValueWithNeitherDefaultNorRequired", caseConfigRefusesAValueWithNeitherDefaultNorRequired)
t.Run("ConfigRefusesAValueWithoutTypeOrDescription", caseConfigRefusesAValueWithoutTypeOrDescription) t.Run("ConfigRefusesAValueWithoutTypeOrDescription", caseConfigRefusesAValueWithoutTypeOrDescription)
t.Run("ConfigStillRefusesAValueNoItemPlaces", caseConfigStillRefusesAValueNoItemPlaces) t.Run("ConfigStillRefusesAValueNoItemPlaces", caseConfigStillRefusesAValueNoItemPlaces)
t.Run("IfConfigNamesADeclaredValueAndPlacesIt", caseIfConfigNamesADeclaredValueAndPlacesIt)
} }
// TestRulesClaims runs the tests of one kind as subtests, each by its former name. // TestRulesClaims runs the tests of one kind as subtests, each by its former name.