The validator judges if-config, config-lines, config-json and a process's bus on requires; grants follow (issue 1979) #211
@@ -724,13 +724,11 @@ func ConfigProblems(m catalogue.Manifest) []string {
|
||||
if strings.TrimSpace(d.Description) == "" {
|
||||
say("no description: say in a sentence what the value is, including its unit (novox/hq ADR 0348 §4)")
|
||||
}
|
||||
switch {
|
||||
case d.Required && d.Default != nil:
|
||||
// A value with neither a default nor `required` is not managed until it is set (design 42 zfs-owns-zfs §11,
|
||||
// answer A-14).
|
||||
if d.Required && d.Default != nil {
|
||||
say("both a default and required: a required value is the operator's own and has no default; give " +
|
||||
"one or the other (novox/hq ADR 0348 §4)")
|
||||
case !d.Required && d.Default == nil:
|
||||
say("neither a default nor required: give a default, or say \"required\": true for a value that is " +
|
||||
"the operator's own (novox/hq ADR 0348 §4)")
|
||||
}
|
||||
if as := renamed[key]; as != "" && d.Required {
|
||||
say("is laid into its file as %q, and only when it is set, so the file and its code fall back when "+
|
||||
@@ -813,15 +811,76 @@ 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 `<key>` or `<key>=<value>` 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 <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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
for _, u := range configLinesUses(m) {
|
||||
placed[u.key] = true
|
||||
}
|
||||
// A file holding ${config-json} writes every config value of the module (design 42 zfs-owns-zfs §9, A-15).
|
||||
for _, r := range m.Resources {
|
||||
if content, _ := r["content"].(string); fmt.Sprint(r["type"]) == "file" && ofConfigJSON.MatchString(content) {
|
||||
for key := range m.Settings {
|
||||
placed[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
@@ -2783,6 +2842,9 @@ 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)...)
|
||||
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).
|
||||
@@ -3936,6 +3998,20 @@ func ProcessBusProblems(m catalogue.Manifest) []string {
|
||||
for _, e := range m.EmitsAll() {
|
||||
emits[e] = true
|
||||
}
|
||||
// What the module asks for in `requires` is its own as well (novox/hq ADR 0595 §1, §2): the events of an events stream
|
||||
// by template, and a key-value state by template (design 42 zfs-owns-zfs, lead answer A-38).
|
||||
for _, e := range m.BusState {
|
||||
switch {
|
||||
case e.Template == catalogue.TemplateEvents && e.Storage == catalogue.StorageStream:
|
||||
var events []string
|
||||
_ = json.Unmarshal(e.Options["events"], &events)
|
||||
for _, ev := range events {
|
||||
emits[ev] = true
|
||||
}
|
||||
case e.Template == catalogue.TemplateState && e.Storage == catalogue.StorageKeyValue:
|
||||
state[e.Name] = true
|
||||
}
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) != "process" {
|
||||
continue
|
||||
@@ -7254,6 +7330,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"}}},
|
||||
@@ -7296,6 +7376,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 {
|
||||
|
||||
@@ -2455,6 +2455,24 @@ func caseAProcessBusNamesOnlyTheModulesOwn(t *testing.T) {
|
||||
t.Errorf("%s: %v; want a refusal naming %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
// Design 42 zfs-owns-zfs, A-38 (V5): what the module asks for in `requires` by template is its own too, an event of
|
||||
// its events stream and a state of its key-value state, and a name it does not ask for there is refused as before.
|
||||
asked := func(bus string) string {
|
||||
raw := mailSenderProcessManifest(t, bus)
|
||||
return strings.Replace(raw, `"resources"`, `"requires":[{"name":"mail-events","storage":"stream","template":"events",`+
|
||||
`"options":{"events":["mail.send.queued"]}},{"name":"mail-kept","storage":"key-value","template":"state"}],"resources"`, 1)
|
||||
}
|
||||
for name, c := range map[string]struct{ bus, says string }{
|
||||
"an event of the events stream asked for": {`{"emits":["mail.send.queued"]}`, ""},
|
||||
"a state asked for by template": {`{"state":["mail-kept"]}`, ""},
|
||||
"an event the events stream does not hold": {`{"emits":["mail.send.lost"]}`, "mail.send.lost"},
|
||||
"a state the module does not ask for": {`{"state":["mail-other"]}`, "mail-other"},
|
||||
} {
|
||||
_, err := ParseManifest([]byte(asked(c.bus)))
|
||||
if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) {
|
||||
t.Errorf("%s: %v; want a refusal naming %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1179/AC4: a bus declaring serves, holds, claims or a seat is refused naming the key; a bus with invokes of
|
||||
@@ -3497,11 +3515,14 @@ func caseConfigRefusesAValueNeitherPlacedNorReadAtRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: a value with neither a default nor required.
|
||||
// Rule 2: a value with both a default and required is refused; one with neither is not managed until set (design 42
|
||||
// zfs-owns-zfs §11, V4/AC1, answer A-14).
|
||||
func caseConfigRefusesAValueWithNeitherDefaultNorRequired(t *testing.T) {
|
||||
m := inConfigForm()
|
||||
delete(declaration(m, "width"), "default")
|
||||
refused(t, m, `"width"`, "neither a default nor required")
|
||||
if _, err := parseRaw(t, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m = inConfigForm()
|
||||
declaration(m, "sender")["default"] = "noreply@example.org"
|
||||
refused(t, m, `"sender"`, "both a default and required")
|
||||
@@ -3532,6 +3553,87 @@ 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")
|
||||
// <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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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"`)
|
||||
}
|
||||
|
||||
// Design 42 zfs-owns-zfs §9, V3/AC1 (A-15, A-24): a file holding ${config-json} places every config value.
|
||||
func caseConfigJSONPlacesEveryValueOfTheModule(t *testing.T) {
|
||||
m := map[string]any{"module": "m", "version": "1",
|
||||
"config": map[string]any{
|
||||
"a": map[string]any{"type": "string", "description": "a word nothing else says", "default": "x"},
|
||||
"b": map[string]any{"type": "number", "description": "a count nothing else says", "default": 1},
|
||||
},
|
||||
"resources": []any{map[string]any{"id": "c", "type": "file", "path": "/x/c.json", "content": "# none\n"}}}
|
||||
refused(t, m, `"a"`, `"b"`, "placed nowhere")
|
||||
resourceOf(m, "c")["content"] = "${config-json}"
|
||||
if _, err := parseRaw(t, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Only in a file's content.
|
||||
m["resources"] = append(m["resources"].([]any), map[string]any{"id": "u", "type": "service", "unit": "u.service", "restart-on": []any{"c"}})
|
||||
resourceOf(m, "u")["unit"] = "x ${config-json}"
|
||||
refused(t, m, "${config-json}")
|
||||
}
|
||||
|
||||
// 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 +6228,9 @@ func TestRulesConfig(t *testing.T) {
|
||||
t.Run("ConfigRefusesAValueWithNeitherDefaultNorRequired", caseConfigRefusesAValueWithNeitherDefaultNorRequired)
|
||||
t.Run("ConfigRefusesAValueWithoutTypeOrDescription", caseConfigRefusesAValueWithoutTypeOrDescription)
|
||||
t.Run("ConfigStillRefusesAValueNoItemPlaces", caseConfigStillRefusesAValueNoItemPlaces)
|
||||
t.Run("IfConfigNamesADeclaredValueAndPlacesIt", caseIfConfigNamesADeclaredValueAndPlacesIt)
|
||||
t.Run("ConfigLinesNamesADeclaredValueAndAPlainPrefix", caseConfigLinesNamesADeclaredValueAndAPlainPrefix)
|
||||
t.Run("ConfigJSONPlacesEveryValueOfTheModule", caseConfigJSONPlacesEveryValueOfTheModule)
|
||||
}
|
||||
|
||||
// TestRulesClaims runs the tests of one kind as subtests, each by its former name.
|
||||
|
||||
@@ -213,6 +213,9 @@ type Principal struct {
|
||||
// Asks says a KindProcess principal reads the records of its module's own asks (novox/hq ADR 0436 §1),
|
||||
// on whichever seat of Uses keeps them (novox/hq ADR 0259 §3) — never set for any other kind.
|
||||
Asks bool
|
||||
// BusStreams, for a KindProcess principal, are the local names of its module's events streams asked for in `requires`
|
||||
// it publishes on (design 42 zfs-owns-zfs, A-38).
|
||||
BusStreams []string
|
||||
|
||||
// SnapshotsTheBus is the bus's own module, the one holding mesh-broker (novox/hq ADR 0235). Its
|
||||
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package buscompose
|
||||
|
||||
import "strings"
|
||||
|
||||
// The bus account of one process of a module that declares bus (novox/hq ADR 0436 §1, §2, §3, §5).
|
||||
//
|
||||
// **Never the module's own account's grants, never the tool runner's.** A process's authority is read
|
||||
@@ -17,6 +19,15 @@ func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
pub = append(pub, own+".event."+e)
|
||||
}
|
||||
|
||||
// 1b. The events of its module's events streams asked for in `requires` (design 42 zfs-owns-zfs, lead answer A-38; ADR
|
||||
// 0595 §1, §2): publish on each stream's subjects, the subject boundOf gives the module's own account, and nothing of
|
||||
// its reading. A name that cannot be one token is granted nothing.
|
||||
for _, local := range p.BusStreams {
|
||||
if safeSubject.MatchString(p.Module) && safeSubject.MatchString(local) && !strings.Contains(local, "_") && !strings.Contains(p.Module, "_") {
|
||||
pub = append(pub, "mesh.bus."+p.Module+"."+local+".>")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. The one durable consumer of each work-queue stream its bus names (novox/hq ADR 0436 §1, §2, §6):
|
||||
// asked about, pulled from and acknowledged, by the same name ConsumersOf composes the consumer under
|
||||
// (QueueConsumer) — and no other stream's.
|
||||
|
||||
@@ -129,6 +129,9 @@ type ProcessPrincipal struct {
|
||||
// Removes are what its account may remove from the bus, as its bus declares them (the indexer's judge refuses any other; novox/hq
|
||||
// ADR 0564 Limits 4): a tools bundle's account, whose whole authority is then BusRemovalGrants.
|
||||
Removes []string
|
||||
// BusStreams are the local names of the events streams its module asks for in `requires` that this process publishes
|
||||
// on, as its bus names their events (design 42 zfs-owns-zfs, A-38; novox/hq ADR 0595 §1, §2).
|
||||
BusStreams []string
|
||||
// State is the module's own state, by local name, this process reads and writes.
|
||||
State []string
|
||||
// Asks says whether this process reads the records of its module's own asks (novox/hq ADR 0259 §3), on
|
||||
@@ -255,7 +258,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
for _, pr := range d.Processes {
|
||||
out = append(out, Principal{
|
||||
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
|
||||
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
||||
Emits: pr.Emits, BusStreams: pr.BusStreams, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
||||
Invokes: pr.Invokes, Removes: pr.Removes,
|
||||
})
|
||||
// The process of the module holding mesh-broker whose bus declares `keeps` keeps every module's bus state
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"mesh-grants/internal/buscompose"
|
||||
)
|
||||
|
||||
// Design 42 zfs-owns-zfs, A-38 (G1): a process whose bus names an event of the events stream its module asks for in
|
||||
// `requires` is granted publish on that stream's subjects, and one naming a state asked for there the reads and writes of
|
||||
// its bucket; a name in the module's own `emits` is granted as before, and what its bus does not name is granted nothing.
|
||||
func TestAProcessBusNamesWhatItsModuleAsksFor(t *testing.T) {
|
||||
var m catalogue.Manifest
|
||||
raw := `{"module":"zfs","version":"1","emits":["scrub.requested"],"requires":[` +
|
||||
`{"name":"events","storage":"stream","template":"events","options":{"events":["config.applied","config.refused"]}},` +
|
||||
`{"name":"applied","storage":"key-value","template":"state"},` +
|
||||
`{"name":"findings","storage":"key-value","template":"state"}],"resources":[` +
|
||||
`{"id":"zfs-keeper","type":"process","run":["./zfs"],"user":"zfs-keeper","bus":` +
|
||||
`{"emits":["config.applied","scrub.requested"],"state":["applied"]}}]}`
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := buscompose.Users(buscompose.Records{Nodes: []string{"ace"}, Assigned: map[string][]buscompose.Declared{
|
||||
"ace": {{Module: "zfs", Emits: m.EmitsAll(), Processes: processesOf(m)}}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var perms buscompose.Permissions
|
||||
for _, u := range users {
|
||||
if u.Kind == buscompose.KindProcess {
|
||||
if perms, err = buscompose.PermissionsFor(u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for subject, want := range map[string]bool{
|
||||
"mesh.bus.zfs.events.config.applied": true,
|
||||
"mesh.mod.zfs.event.config.applied": false,
|
||||
"mesh.mod.zfs.event.scrub.requested": true,
|
||||
"$KV.zfs_applied.pool.tank": true,
|
||||
"$KV.zfs_findings.pool.tank": false,
|
||||
"mesh.bus.zfs.findings.config.applied": false,
|
||||
} {
|
||||
if got := buscompose.MayPublish(perms, subject); got != want {
|
||||
t.Errorf("publish on %s: %v, want %v", subject, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"maps"
|
||||
@@ -401,12 +402,37 @@ func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal {
|
||||
return nil
|
||||
}
|
||||
queues := queueOwnersOf(m)
|
||||
// The events of each events stream the module asks for in `requires` by template, by the stream's local name: an event
|
||||
// of a process's bus found there, and not among the module's own `emits`, is published on that stream's subjects, and a
|
||||
// state asked for there is granted through `State` as a module's own is (design 42 zfs-owns-zfs, lead answer A-38; ADR
|
||||
// 0595 §1, §2).
|
||||
streamOf := map[string]string{}
|
||||
for _, e := range m.BusState {
|
||||
if e.Template != catalogue.TemplateEvents || e.Storage != catalogue.StorageStream {
|
||||
continue
|
||||
}
|
||||
var events []string
|
||||
_ = json.Unmarshal(e.Options["events"], &events)
|
||||
for _, ev := range events {
|
||||
streamOf[ev] = e.Name
|
||||
}
|
||||
}
|
||||
own := m.EmitsAll()
|
||||
var out []buscompose.ProcessPrincipal
|
||||
// Every own bus account, processes first and then tools bundles (catalogue.BusAccounts): a tools bundle's account carries
|
||||
// what it may remove from the bus (ADR 0564 Limits 4, issue 1952 S5).
|
||||
for _, a := range accounts {
|
||||
pr := buscompose.ProcessPrincipal{Process: a.ID, Emits: a.Bus.Emits, State: a.Bus.State, Asks: a.Bus.Asks,
|
||||
pr := buscompose.ProcessPrincipal{Process: a.ID, State: a.Bus.State, Asks: a.Bus.Asks,
|
||||
Invokes: a.Bus.Invokes, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
|
||||
for _, e := range a.Bus.Emits {
|
||||
if local, asked := streamOf[e]; asked && !slices.Contains(own, e) {
|
||||
if !slices.Contains(pr.BusStreams, local) {
|
||||
pr.BusStreams = append(pr.BusStreams, local)
|
||||
}
|
||||
continue
|
||||
}
|
||||
pr.Emits = append(pr.Emits, e)
|
||||
}
|
||||
for _, q := range queues {
|
||||
if q.Process == a.ID {
|
||||
pr.Queues = append(pr.Queues, q)
|
||||
|
||||
Reference in New Issue
Block a user