diff --git a/internal/broker/announced.go b/internal/broker/announced.go index dd0ec5f9..e05f7835 100644 --- a/internal/broker/announced.go +++ b/internal/broker/announced.go @@ -20,7 +20,7 @@ const StatesAnnouncedBucket = announce.KeptBucket // among them, which the pacman module reads, ADR 0507 §5), the chain's buckets and the grants issued, which the assigner // reads (ADR 0528 clause 2). var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts, - MirrorDeclaredPackages, ControllerGrants} + MirrorDeclaredPackages, ControllerGrants, ControllerComposed} // ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local // name, the ones others may read marked readable. It announces itself, as any module does from its code. diff --git a/internal/broker/announced_test.go b/internal/broker/announced_test.go index f538c557..ee4a600a 100644 --- a/internal/broker/announced_test.go +++ b/internal/broker/announced_test.go @@ -39,7 +39,7 @@ func TestTheControllerAnnouncesWhatItKeeps(t *testing.T) { } } want := []string{ChainBuilds, MirrorDeclaredConfig, MirrorPredefined, ChainRollouts, - MirrorAssignments, MirrorDeclaredPackages, ControllerGrants} + MirrorAssignments, MirrorDeclaredPackages, ControllerGrants, ControllerComposed} slices.Sort(want) if !slices.Equal(readable, want) { t.Fatalf("readable %v, want %v", readable, want) diff --git a/internal/broker/composed.go b/internal/broker/composed.go new file mode 100644 index 00000000..2053307e --- /dev/null +++ b/internal/broker/composed.go @@ -0,0 +1,96 @@ +package broker + +import ( + "context" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "github.com/nats-io/nats.go/jetstream" +) + +// Release 6's grants, carried ahead of it (novox/hq ADR 0539 clauses 2, 6, 9 and 16; issue 1945, the review's A1). The +// switch to release 6 is the last delivery plan of today's controller, and the bus's user list after it is the one +// today's controller composes: so the subjects the new controller, every new node-engine and the assigner use after the +// switch are granted by this controller already, before the group merges. Unused until then: nothing writes or reads +// these buckets before release 6. +// +// Names as mesh-sdk's release 6 gives them (delivery.ComposedBucketFor, ControllerComposedBucket); written out here +// because this controller's mesh-sdk predates them, and held equal by release 6's own tests. + +// ComposedBucketPrefix starts a node's bucket of compositions, `composed-`. +const ComposedBucketPrefix = "composed-" + +// ControllerComposed is the local name of the compositions' digests, `mesh-controller_composed`, which the assigner reads. +const ControllerComposed = "composed" + +// ControllerComposedBucket is the bucket of the compositions' digests. +var ControllerComposedBucket = BucketName(ControllerSeat, ControllerComposed) + +// composedBucketOf is a node's bucket of compositions; a node name a subject would widen is refused. +func composedBucketOf(node string) (string, error) { + if !safeSubject.MatchString(node) { + return "", fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node) + } + return ComposedBucketPrefix + node, 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 := composedBucketOf(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: every key of its own +// composition bucket, and its own wants (`want..`, `want..node`) in the assigner's bucket; nothing +// of another node's, nor the digests. +func ComposedReads(node string) ([]string, error) { + bucket, err := composedBucketOf(node) + if err != nil { + return nil, err + } + return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil +} + +// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want: its +// backup holder backs a data module up before it moves, and says when it was (ADR 0539 clause 16). +var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}} + +// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node: `mesh.seat..tool..`. +func NodeAgentAsks(node string) ([]string, error) { + 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) + } + var out []string + for _, v := range VerbsTheNodeAgentAsks { + out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node) + } + return out, nil +} + +// NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a +// backup is taken, from whichever module holds node-backup on its machine. +var NodeAgentHears = []string{"mesh.mod.*.event.backed-up"} + +// ComposedDigestsConfig is the bucket of the compositions' digests as the controller asserts it: five revisions a key. +func ComposedDigestsConfig() jetstream.KeyValueConfig { + return jetstream.KeyValueConfig{ + Bucket: 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; empty until release 6", + History: 5, + MaxValueSize: 4 << 10, + MaxBytes: 64 << 20, + Storage: jetstream.FileStorage, + } +} + +// EnsureComposedDigests asserts the bucket of the compositions' digests. +func EnsureComposedDigests(ctx context.Context, js jetstream.JetStream) error { + if _, err := js.CreateOrUpdateKeyValue(ctx, ComposedDigestsConfig()); err != nil { + return fmt.Errorf("asserting bucket %s: %w", ControllerComposedBucket, err) + } + return nil +} diff --git a/internal/broker/composed_test.go b/internal/broker/composed_test.go new file mode 100644 index 00000000..b0cc5b37 --- /dev/null +++ b/internal/broker/composed_test.go @@ -0,0 +1,60 @@ +package broker + +import ( + "slices" + "strings" + "testing" + + "github.com/nats-io/nats.go/jetstream" +) + +// 1945 (the review's A1): the bus's user list today's controller composes, which is the one the bus runs after release +// 6's switch, already grants every subject the new controller, every new node-engine and the assigner use: the +// controller writes each node's compositions and the digests; a node-engine reads its own compositions and wants, asks +// its own backup holder and hears its word, and nothing of another node's; the assigner reads the digests, announced +// readable. +func TestTheUserListBeforeTheSwitchGrantsWhatReleaseSixUses(t *testing.T) { + controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x", FilesFor: []string{"ace", "g14"}}) + if err != nil { + t.Fatal(err) + } + for _, s := range []string{"$KV.composed-ace.>", "$KV.composed-g14.>", "$KV.mesh-controller_composed.>"} { + has(t, controller.Publish, s) + } + node, err := PermissionsFor(Principal{Kind: KindNode, Node: "ace", PasswordHash: "x"}) + if err != nil { + t.Fatal(err) + } + for _, s := range []string{ + "$JS.API.STREAM.INFO.KV_composed-ace", "$JS.API.DIRECT.GET.KV_composed-ace.>", "$JS.API.CONSUMER.CREATE.KV_composed-ace.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.>", + "mesh.seat.node-backup.tool.now.ace", "mesh.seat.node-backup.tool.backed-up.ace", + } { + has(t, node.Publish, s) + } + has(t, node.Subscribe, "mesh.mod.*.event.backed-up") + for _, p := range node.Publish { + if strings.Contains(p, "composed-g14") || strings.Contains(p, "mesh-controller_composed") || strings.Contains(p, "want.g14") || + strings.HasPrefix(p, "$KV.composed-") || strings.HasSuffix(p, "node-backup.tool.now.*") { + t.Errorf("a node-engine of ace may publish %s", p) + } + } + if !slices.Contains(controllerReadableStates(), ControllerComposed) || + !slices.Contains(ControllerBuckets(), ControllerComposedBucket) || !IsControllerBucket(ControllerComposedBucket) { + t.Fatalf("the digests are not announced readable among the controller's buckets") + } + if !ControllerAnnouncement("novox").Readable(ControllerComposed) { + t.Fatalf("the controller does not announce %s readable", ControllerComposed) + } + assigner, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-delivery-assigner", PasswordHash: "x", + Reads: []string{"mesh-controller.composed"}}) + if err != nil { + t.Fatal(err) + } + has(t, assigner.Publish, "$JS.API.STREAM.INFO.KV_mesh-controller_composed") + has(t, assigner.Publish, "$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.>") + if c := ComposedDigestsConfig(); c.Bucket != "mesh-controller_composed" || c.History != 5 || c.Storage != jetstream.FileStorage { + t.Errorf("the digests' bucket is %+v", c) + } +} diff --git a/internal/broker/controller_buckets.go b/internal/broker/controller_buckets.go index f7dd0a2b..f6f00782 100644 --- a/internal/broker/controller_buckets.go +++ b/internal/broker/controller_buckets.go @@ -196,14 +196,15 @@ func IsControllerBucket(bucket string) bool { bucket == MergeWindowBucket || bucket == RolloutSilentBucket || bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket || bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket || - bucket == GrantsBucket + bucket == GrantsBucket || bucket == ControllerComposedBucket } // ControllerBuckets are the controller's own buckets, in the order they are asserted. func ControllerBuckets() []string { return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket, AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket, - DeliveryMergesBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket, GrantsBucket} + DeliveryMergesBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket, GrantsBucket, + ControllerComposedBucket} } // ControllerBucketsAsserter is what raising the controller's buckets needs of a connection. @@ -331,6 +332,10 @@ func (j *JetStream) EnsureControllerBuckets() error { }); err != nil { return fmt.Errorf("asserting bucket %s: %w", RolloutSilentBucket, err) } + // **The compositions' digests** (ADR 0539 clauses 2 and 6), asserted ahead of release 6: what its assigner reads. + if err := EnsureComposedDigests(ctx, js); err != nil { + return err + } // **The states each module announced** (novox/hq ADR 0495 clauses 2 and 3, issue 1790): one key a module, its last word. if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ Bucket: StatesAnnouncedBucket, diff --git a/internal/broker/controller_buckets_test.go b/internal/broker/controller_buckets_test.go index 799a46d7..0c7441cd 100644 --- a/internal/broker/controller_buckets_test.go +++ b/internal/broker/controller_buckets_test.go @@ -115,7 +115,8 @@ func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) { module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"} rowed := map[string]bool{} for _, row := range WritersTable { - if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" { + if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" && + row.State != "the compositions' digests" { continue } for _, subject := range row.Subjects { diff --git a/internal/broker/nats.go b/internal/broker/nats.go index e55ba60f..dbac4676 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -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." + ControllerComposedBucket + ".>", + Reason: "carried only at genesis: nothing writes a composition's digest before release 6, whose controller " + + "composes its own permissions into the bus module's composition (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,6 +743,12 @@ func PermissionsFor(p Principal) (Permissions, error) { return Permissions{}, err } pub = append(pub, writes) + // And each node's compositions, which it alone writes from release 6 on (novox/hq ADR 0539 clause 2). + composed, err := ComposedWrites(node) + if err != nil { + return Permissions{}, err + } + pub = append(pub, composed) } // **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 @@ -845,6 +856,19 @@ func PermissionsFor(p Principal) (Permissions, error) { return Permissions{}, err } pub = append(pub, fileReads...) + // And, from release 6 on, its own compositions and wants, its own backup holder's verbs and word (novox/hq ADR + // 0539 clauses 2, 9 and 16): granted ahead, since today's controller composes the user list after the switch. + composedReads, err := ComposedReads(p.Node) + if err != nil { + return Permissions{}, err + } + pub = append(pub, composedReads...) + backups, err := NodeAgentAsks(p.Node) + if err != nil { + return Permissions{}, err + } + pub = append(pub, backups...) + sub = append(sub, NodeAgentHears...) // 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 { diff --git a/internal/broker/testdata/composed.conf b/internal/broker/testdata/composed.conf index a6a80775..99e433b1 100644 --- a/internal/broker/testdata/composed.conf +++ b/internal/broker/testdata/composed.conf @@ -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,8 +38,8 @@ 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.>"] } - 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"] } + 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.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one"] } + subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.*.event.backed-up", "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" } } } { user: "one.mesh-delivery-watchman", password: "$2a$11$wwwwwwwwwwwwwwwwwwwwww", permissions: { diff --git a/internal/broker/writers.go b/internal/broker/writers.go index b904ed1e..78e27196 100644 --- a/internal/broker/writers.go +++ b/internal/broker/writers.go @@ -152,6 +152,10 @@ var WritersTable = []WriterRow{ DeliveryMergesBucket, Others: "read through plans", Subjects: append(append(kvOf(MergeWindowBucket), kvOf(RolloutSilentBucket)...), kvOf(DeliveryMergesBucket)...), Writes: isController}, + // The compositions' digests, granted and asserted ahead of release 6, which writes them (ADR 0539 clauses 2 and 6). + {State: "the compositions' digests", Writer: "controller (lease holder), compare-and-set, from release 6", + KeptIn: "key-value " + ControllerComposedBucket, Others: "the assigner reads them", + Subjects: kvOf(ControllerComposedBucket), Writes: isController}, {State: "conditions", Writer: "controller", KeptIn: "key-value " + ConditionsBucket + " (and its history, " + ConditionHistoryBucket + "); how often each was observed, when last and its newest evidence in the " + "controller's store, so a look that changes nothing writes no bucket", Others: "raise or clear only through observations the controller reads", diff --git a/internal/broker/writers_test.go b/internal/broker/writers_test.go index f1e55584..9614f3ba 100644 --- a/internal/broker/writers_test.go +++ b/internal/broker/writers_test.go @@ -21,6 +21,8 @@ var designRows = []string{ "a machine's applied state and its report", "the controller lease", "plans and their tiers", + // Release 6's compositions' digests, granted ahead of it (ADR 0539 clauses 2 and 6, issue 1945). + "the compositions' digests", "conditions", "calls and their outcomes", "the hand-act log",