Write each module's composition per node, signed, with its digest and the move's judgement beside it (issue 1945, B1)
Every act that sent a declaration now writes compositions instead, by compare-and-set under the lease, and a module's compositions are kept for its five newest fingerprints and whatever its want or have names (ADR 0539 clauses 1 to 5 and 14). A module the mesh provides itself, which no want can name, rides in the node's own.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4): one per node, `composed-<node>`, holding that node's
|
||||
// compositions, which only that node's node-engine reads, since a composition can carry a sealed value (ADR 0488 §2's
|
||||
// reason); and `mesh-controller_composed`, holding each composition's digest and the judgement of its move, which the
|
||||
// assigner reads. The controller writes both, by compare-and-set while it holds the lease; each keeps five revisions
|
||||
// of a key. The names and layouts are mesh-sdk's (delivery.ComposedBucketFor, ControllerComposedBucket, ComposedKey).
|
||||
|
||||
// ComposedValueBytes is the most a composition may hold: a file over 64 KiB travels by digest, so a module's
|
||||
// composition is far smaller; just under the server's largest message, which also carries the subject and headers.
|
||||
const ComposedValueBytes = 1<<20 - 8<<10
|
||||
|
||||
// ComposedBucketBytes is a node's composition bucket's size: five revisions of every module's compositions of the node.
|
||||
const ComposedBucketBytes = 256 << 20
|
||||
|
||||
// ComposedDigestsBytes is the size of the bucket of digests: a value of a few hundred bytes per node, module and
|
||||
// fingerprint kept.
|
||||
const ComposedDigestsBytes = 64 << 20
|
||||
|
||||
// ComposedConfig is a node's composition bucket as the controller asserts it.
|
||||
func ComposedConfig(node string) (jetstream.KeyValueConfig, error) {
|
||||
bucket, err := delivery.ComposedBucketFor(node)
|
||||
if err != nil {
|
||||
return jetstream.KeyValueConfig{}, err
|
||||
}
|
||||
return jetstream.KeyValueConfig{
|
||||
Bucket: bucket,
|
||||
Description: "the compositions of " + node + " (novox/hq ADR 0539): each module's resources for this node at one " +
|
||||
"fingerprint, and the node's own, signed; written by the controller alone, read by this node's node-engine " +
|
||||
"alone; deleted when outside the module's five newest fingerprints and named by no want",
|
||||
History: delivery.ComposedHistory,
|
||||
MaxValueSize: ComposedValueBytes,
|
||||
MaxBytes: ComposedBucketBytes,
|
||||
Storage: jetstream.FileStorage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ComposedDigestsConfig is the bucket of the compositions' digests as the controller asserts it.
|
||||
func ComposedDigestsConfig() jetstream.KeyValueConfig {
|
||||
return jetstream.KeyValueConfig{
|
||||
Bucket: delivery.ControllerComposedBucket,
|
||||
Description: "each composition's digest and the judgement of its move from what the node runs (novox/hq ADR 0539 " +
|
||||
"clauses 2 and 14): written by the controller alone, read by the assigner",
|
||||
History: delivery.ControllerComposedHistory,
|
||||
MaxValueSize: 4 << 10,
|
||||
MaxBytes: ComposedDigestsBytes,
|
||||
Storage: jetstream.FileStorage,
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureComposed creates the bucket of digests and each named node's composition bucket if absent and brings their
|
||||
// options to match. An update, never a delete.
|
||||
func EnsureComposed(ctx context.Context, js jetstream.JetStream, nodes ...string) error {
|
||||
if _, err := js.CreateOrUpdateKeyValue(ctx, ComposedDigestsConfig()); err != nil {
|
||||
return fmt.Errorf("asserting bucket %s: %w", delivery.ControllerComposedBucket, err)
|
||||
}
|
||||
for _, node := range nodes {
|
||||
cfg, err := ComposedConfig(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := js.CreateOrUpdateKeyValue(ctx, cfg); err != nil {
|
||||
return fmt.Errorf("asserting bucket %s: %w", cfg.Bucket, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComposedWrites is what the controller publishes to write a node's compositions: the bucket's own subjects.
|
||||
func ComposedWrites(node string) (string, error) {
|
||||
bucket, err := delivery.ComposedBucketFor(node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "$KV." + bucket + ".>", nil
|
||||
}
|
||||
|
||||
// ComposedReads is what a node's node-engine publishes to follow its wants and compositions (ADR 0539 clauses 1, 2 and
|
||||
// 9): every key of its own composition bucket, and its own wants (`want.<node>.<module>` and `want.<node>.node`) in the
|
||||
// assigner's bucket; nothing of another node's, nor the bucket of digests.
|
||||
func ComposedReads(node string) ([]string, error) {
|
||||
bucket, err := delivery.ComposedBucketFor(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !safeSubject.MatchString(node) {
|
||||
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
|
||||
}
|
||||
return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// 1945/AC1: a node's composition bucket keeps five revisions of a key and is named for its node; the bucket of digests
|
||||
// likewise; a node name a subject would widen is refused.
|
||||
func TestTheCompositionBuckets(t *testing.T) {
|
||||
cfg, err := ComposedConfig("ace")
|
||||
if err != nil || cfg.Bucket != "composed-ace" || cfg.History != 5 {
|
||||
t.Fatalf("ComposedConfig(ace) = %+v, %v", cfg, err)
|
||||
}
|
||||
if d := ComposedDigestsConfig(); d.Bucket != delivery.ControllerComposedBucket || d.History != 5 {
|
||||
t.Errorf("ComposedDigestsConfig = %+v", d)
|
||||
}
|
||||
if _, err := ComposedConfig("a.b"); err == nil {
|
||||
t.Errorf("a dotted node got a bucket")
|
||||
}
|
||||
}
|
||||
|
||||
// 1945/AC1: a node-engine reads its own compositions and its own wants and nothing of another node's, nor the digests;
|
||||
// the controller writes a node's bucket by its own subjects.
|
||||
func TestANodeReadsOnlyItsOwnCompositions(t *testing.T) {
|
||||
reads, err := ComposedReads("ace")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := strings.Join(reads, " ")
|
||||
for _, want := range []string{"KV_composed-ace", "$KV.mesh-delivery-assigner_wants.want.ace.>"} {
|
||||
if !strings.Contains(all, want) {
|
||||
t.Errorf("a node-engine of ace cannot reach %s: %v", want, reads)
|
||||
}
|
||||
}
|
||||
for _, never := range []string{"composed-g14", delivery.ControllerComposedBucket, "$KV.composed-ace", "want.*", "want.>"} {
|
||||
if strings.Contains(all, never) {
|
||||
t.Errorf("a node-engine of ace is granted %s: %v", never, reads)
|
||||
}
|
||||
}
|
||||
if w, err := ComposedWrites("ace"); err != nil || w != "$KV.composed-ace.>" {
|
||||
t.Errorf("ComposedWrites(ace) = %q, %v", w, err)
|
||||
}
|
||||
if _, err := ComposedReads("a*"); err == nil {
|
||||
t.Errorf("a node name with a wildcard was granted")
|
||||
}
|
||||
}
|
||||
@@ -427,6 +427,11 @@ var DegradableGrants = []DegradableGrant{
|
||||
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
|
||||
"first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)",
|
||||
},
|
||||
{
|
||||
Subject: "$KV." + delivery.ControllerComposedBucket + ".>",
|
||||
Reason: "carried only at genesis: the controller composes its own permissions into the bus module's composition, " +
|
||||
"and writes a composition's digest only after the bus applied it (novox/hq ADR 0539 clauses 2 and 7)",
|
||||
},
|
||||
{
|
||||
Subject: "$KV." + GrantsBucket + ".>",
|
||||
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
|
||||
@@ -738,7 +743,15 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, writes)
|
||||
// And each node's compositions, which it alone writes (novox/hq ADR 0539 clause 2).
|
||||
composed, err := ComposedWrites(node)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, composed)
|
||||
}
|
||||
// And the compositions' digests, which the assigner reads (ADR 0539 clauses 2 and 6).
|
||||
pub = append(pub, "$KV."+delivery.ControllerComposedBucket+".>")
|
||||
// **And what the bus says about itself, read-only** (novox/hq to-be 45 §3, S9): a durable
|
||||
// consumer that gave up on a message, or one that was deleted. The server already publishes
|
||||
// both in the mesh's own account; the controller says each as a condition in the mesh's words.
|
||||
@@ -845,6 +858,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, fileReads...)
|
||||
// And its own compositions and wants, which it follows (novox/hq ADR 0539 clauses 1, 2 and 9), and nothing of another
|
||||
// node's.
|
||||
composedReads, err := ComposedReads(p.Node)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, composedReads...)
|
||||
// And, while the vault serves it, the vault's move-in in its own name and nobody else's (issue 676): the
|
||||
// values sealed to this machine, which only it opens, handed in sealed to the vault's key for the call.
|
||||
if p.MovesIntoTheVault {
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ accounts {
|
||||
]
|
||||
users = [
|
||||
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
|
||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
|
||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_composed.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
|
||||
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
@@ -38,7 +38,7 @@ accounts {
|
||||
subscribe: { allow: ["_INBOX.enrol.one.>"] }
|
||||
} }
|
||||
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
||||
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
|
||||
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
|
||||
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
|
||||
Reference in New Issue
Block a user