The manifest's reading of if-config, config-lines and config-json, and a call onto a bundle's own events stream (issue 1979) #190
@@ -31,6 +31,8 @@ const (
|
||||
|
||||
StreamRead = "mesh/stream.read"
|
||||
StreamFill = "mesh/stream.fill"
|
||||
// StreamPublish says one event on the module's own bound stream (A-42).
|
||||
StreamPublish = "mesh/stream.publish"
|
||||
|
||||
WorkQueueSize = "mesh/workqueue.size"
|
||||
WorkQueueDead = "mesh/workqueue.dead"
|
||||
@@ -153,6 +155,20 @@ type StreamPage struct {
|
||||
Last uint64 `json:"last"`
|
||||
}
|
||||
|
||||
// StreamPublishRequest says one event on a module's own stream, by the stream's local name: the tool runner publishes
|
||||
// Body on the binding's subject plus "." plus Event, acknowledged, with ID as the message id (design 42 zfs-owns-zfs, A-42).
|
||||
type StreamPublishRequest struct {
|
||||
Stream string `json:"stream"`
|
||||
Event string `json:"event"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// StreamPublished is publish's answer: the sequence the stream acknowledged the event at.
|
||||
type StreamPublished struct {
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
// StreamFilled is fill's answer: the stream's fill and its first and last sequence.
|
||||
type StreamFilled struct {
|
||||
Fill
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package buscalls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A-42: stream.publish is a method of its own, beside read and fill, and its request and answer keep their field names.
|
||||
func TestStreamPublishIsAMethodWithItsShapes(t *testing.T) {
|
||||
if StreamPublish != "mesh/stream.publish" || StreamPublish == StreamRead || StreamPublish == StreamFill {
|
||||
t.Fatalf("method %q", StreamPublish)
|
||||
}
|
||||
b, _ := json.Marshal(StreamPublishRequest{Stream: "events", Event: "scrub.requested", Body: json.RawMessage(`{"a":1}`), ID: "x"})
|
||||
if string(b) != `{"stream":"events","event":"scrub.requested","body":{"a":1},"id":"x"}` {
|
||||
t.Errorf("request %s", b)
|
||||
}
|
||||
a, _ := json.Marshal(StreamPublished{Sequence: 7})
|
||||
if string(a) != `{"sequence":7}` {
|
||||
t.Errorf("answer %s", a)
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,15 @@ const (
|
||||
// referenceForms are the reference forms a manifest's values may hold, `${<name>:…}`, as this contract fills them. A new
|
||||
// reference form is added here in the change that teaches the contract to read it, so a node-engine built with it
|
||||
// states it (ADR 0612 §2's new reference form is one).
|
||||
var referenceForms = []string{"access", "backup", "bound", "collected", "config", "consumer", "consumers",
|
||||
"contribution", "dir", "machine", "port", "seat", "secret", "setting"}
|
||||
var referenceForms = []string{"access", "backup", "bound", "collected", "config", "config-json", "config-lines",
|
||||
"consumer", "consumers", "contribution", "dir", "machine", "port", "seat", "secret", "setting"}
|
||||
|
||||
// ifConfigWord is the reader capability for a resource's `if-config` (IfConfigField).
|
||||
const ifConfigWord = ReadWord + "resources." + IfConfigField
|
||||
|
||||
// configJSONToken is the reference form `${config-json}` whole: it holds no colon, so textNeeds's search for `${<form>:`
|
||||
// does not find it (design 42 zfs-owns-zfs, answer A-25).
|
||||
const configJSONToken = "${config-json}"
|
||||
|
||||
// ReferenceFormSeat names the reference form a registry seat answers for a build's artifact,
|
||||
// `<seat>/<module>/<artifact>@sha256:<digest>` (novox/hq ADR 0605 §9; the registry clients' reference forms), as the reader capability
|
||||
@@ -106,6 +113,7 @@ func ManifestReads() []string {
|
||||
set[ReadForm+f] = true
|
||||
}
|
||||
set[ReadForm+ReferenceFormSeatReach] = true
|
||||
set[ifConfigWord] = true
|
||||
for _, s := range Seats() {
|
||||
set[ReadSeat+s.Name] = true
|
||||
}
|
||||
@@ -171,6 +179,13 @@ func ManifestNeeds(raw []byte) ([]string, error) {
|
||||
if claimsAtMesh(top, storeHolderSeat) {
|
||||
used[ReadForm+ReferenceFormSeatReach] = true
|
||||
}
|
||||
for _, r := range asList(top["resources"]) {
|
||||
if obj, ok := r.(map[string]any); ok {
|
||||
if _, has := obj[IfConfigField]; has {
|
||||
used[ifConfigWord] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
|
||||
used[ReadForm+"config"] = true
|
||||
}
|
||||
@@ -437,6 +452,9 @@ func textNeeds(text []byte, used map[string]bool) {
|
||||
used[ReadForm+f] = true
|
||||
}
|
||||
}
|
||||
if bytes.Contains(text, []byte(configJSONToken)) {
|
||||
used[ReadForm+"config-json"] = true
|
||||
}
|
||||
if ofSeatReach.Match(text) {
|
||||
used[ReadForm+ReferenceFormSeatReach] = true
|
||||
}
|
||||
|
||||
@@ -96,10 +96,25 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
|
||||
{"a template naming a seat's reach", `{"module":"d","version":"1","resources":[{"id":"daemon","type":"file",` +
|
||||
`"path":"/etc/d.json","template":"files/daemon.json"}]}`, []string{"form:seat", "form:seat-reach"},
|
||||
[]string{"form:seat-reach"}},
|
||||
// A resource under a config condition (design 42 zfs-owns-zfs §4, A-13).
|
||||
{"a resource under if-config", `{"module":"z","version":"1","config":{"k":{"type":"string"}},"resources":[{"id":"f","type":"file",` +
|
||||
`"if-config":["k"]}]}`, []string{"word:resources.if-config"}, []string{"word:resources.if-config"}},
|
||||
{"a resource without if-config", `{"module":"z","version":"1","resources":[{"id":"f","type":"file"}]}`,
|
||||
[]string{"word:resources"}, nil},
|
||||
// The template forms of the config file and the option lines (design 42 zfs-owns-zfs §3, §9; A-13, A-15, A-25).
|
||||
{"a template writing config lines", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/o.conf"}]}`,
|
||||
[]string{"form:config-lines"}, []string{"form:config-lines"}},
|
||||
{"a template writing the config as json", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/c.json"}]}`,
|
||||
[]string{"form:config-json"}, []string{"form:config-json"}},
|
||||
{"a template of the config form alone", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/k"}]}`,
|
||||
[]string{"form:config"}, nil},
|
||||
{"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil},
|
||||
} {
|
||||
templates := map[string]map[string]string{ // the templates' text a row's module names
|
||||
"a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`},
|
||||
"a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`},
|
||||
"a template writing config lines": {"files/o.conf": "${config-lines:arc-max:options zfs zfs_arc_max=:mib-bytes}"},
|
||||
"a template writing the config as json": {"files/c.json": "${config-json}"},
|
||||
"a template of the config form alone": {"files/k": "${config:k}"},
|
||||
}[row.why]
|
||||
needs, err := ModuleNeeds([]byte(row.raw), templates)
|
||||
if err != nil {
|
||||
@@ -119,6 +134,15 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
|
||||
if slices.ContainsFunc(needs, func(n string) bool { return n == "word:newer-word" || n == "form:count" }) {
|
||||
t.Errorf("%s: needs %v, naming what the contract does not read", row.why, needs)
|
||||
}
|
||||
for _, f := range []string{"form:config-lines", "form:config-json"} {
|
||||
if slices.Contains(needs, f) != (row.why == map[string]string{"form:config-lines": "a template writing config lines",
|
||||
"form:config-json": "a template writing the config as json"}[f]) {
|
||||
t.Errorf("%s: needs %v, %s read wrongly", row.why, needs, f)
|
||||
}
|
||||
}
|
||||
if slices.Contains(needs, "word:resources.if-config") != (row.why == "a resource under if-config") {
|
||||
t.Errorf("%s: needs %v, if-config read wrongly", row.why, needs)
|
||||
}
|
||||
if got := MissingManifestReads(needs, nil); !slices.Equal(got, row.missing) {
|
||||
t.Errorf("%s: an engine stating nothing lacks %v, want %v", row.why, got, row.missing)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
// catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly.
|
||||
const TrustedField = "trusted"
|
||||
|
||||
// IfConfigField is the key any resource may carry to be declared only while a config value of its module holds
|
||||
// (novox/hq design 42 zfs-owns-zfs §2, §4): a list of conditions, `<key>` or `<key>=<value>`. Said in the catalogue and
|
||||
// read by the node-engine, which states it as the reader capability `word:resources.if-config` (ManifestReads); a
|
||||
// manifest whose resource carries it needs that capability (ManifestNeeds).
|
||||
const IfConfigField = "if-config"
|
||||
|
||||
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
|
||||
// accesses, every key its provisions serve and every setting a served value asks for, every setting a file
|
||||
// asks for unless it says `"trusted": false`, and every setting a directory's owner asks for. Places and accesses first, then the rest sorted.
|
||||
|
||||
@@ -265,6 +265,7 @@ func TestModuleAccountNamesAndDialOptions(t *testing.T) {
|
||||
for _, c := range []struct{ name, got, want string }{
|
||||
{"event subject", a.EventSubject("put"), "mesh.mod.mesh-delivery-teller.event.put"},
|
||||
{"own stream subject", a.StreamSubject("forge.x.pull"), "mesh.stream.mesh-delivery-teller.event.forge.x.pull"},
|
||||
{"template stream subject", a.BusSubject("events", "snapshot.taken.person"), "mesh.bus.mesh-delivery-teller.events.snapshot.taken.person"},
|
||||
{"another module's stream subject", ModuleStreamSubject("mesh-delivery-checker", "check"), "mesh.stream.mesh-delivery-checker.event.check"},
|
||||
{"durable consumer", a.EventConsumerName(), "n1_mesh-delivery-teller"},
|
||||
} {
|
||||
|
||||
@@ -71,6 +71,12 @@ func (a Account) EventSubject(event string) string { return "mesh.mod." + a.Modu
|
||||
// (mesh-controller internal/broker/modstreams.go ModuleStreamSubject).
|
||||
func (a Account) StreamSubject(event string) string { return ModuleStreamSubject(a.Module, event) }
|
||||
|
||||
// BusSubject is where the module's event lands on a stream it asked for by template: mesh.bus.<module>.<local>.<event>,
|
||||
// <local> being the stream's local name in the module's `requires` (the bus module's keeper, PlanOf; A-41).
|
||||
func (a Account) BusSubject(local, event string) string {
|
||||
return "mesh.bus." + a.Module + "." + local + "." + event
|
||||
}
|
||||
|
||||
// ModuleStreamSubject is where a module's event lands on that module's own stream, for a part that names another
|
||||
// module's stream (the indexer hears the forge bridge's, the checker's) without holding its credential.
|
||||
func ModuleStreamSubject(module, event string) string {
|
||||
|
||||
Reference in New Issue
Block a user