diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 8caf1284..12cf6863 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -813,10 +813,60 @@ func TypedSettingProblems(m catalogue.Manifest) []string { return problems } -// configPlaced is every value a file, a unit, a directory's owner, a contribution or a served fact asks for, and -// every value a mergeable file takes. +// ifConfigKeys is the keys a resource's `if-config` names, each `` or `=` by its key, and what is +// 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 and = "+ + "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 { placed := settingKeysUsedBy(m) + for _, r := range m.Resources { + keys, _ := ifConfigKeys(r) + for _, k := range keys { + placed[k] = true + } + } for _, r := range m.Resources { for _, k := range takenKeyList(r) { placed[k] = true @@ -2783,6 +2833,8 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M problems = append(problems, SettingProblems(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). problems = append(problems, OptionalProblems(m)...) // And which setting keys each mergeable file takes (setting_takes.go, novox/hq ADR 0326). diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 2632de4b..da7661c9 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -3532,6 +3532,37 @@ func caseConfigStillRefusesAValueNoItemPlaces(t *testing.T) { 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") + // and = 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 // provision's provider protects its consumers' data. func caseConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) { @@ -6126,6 +6157,7 @@ func TestRulesConfig(t *testing.T) { t.Run("ConfigRefusesAValueWithNeitherDefaultNorRequired", caseConfigRefusesAValueWithNeitherDefaultNorRequired) t.Run("ConfigRefusesAValueWithoutTypeOrDescription", caseConfigRefusesAValueWithoutTypeOrDescription) t.Run("ConfigStillRefusesAValueNoItemPlaces", caseConfigStillRefusesAValueNoItemPlaces) + t.Run("IfConfigNamesADeclaredValueAndPlacesIt", caseIfConfigNamesADeclaredValueAndPlacesIt) } // TestRulesClaims runs the tests of one kind as subtests, each by its former name.