Files
mesh-controller/internal/broker/composed.go
T
jschoubben 1350c428ed Let the operator's hand path replace a file over 64 KiB in a node's object bucket (issue 1945; the review of A1, H1)
The object store reads an object's metadata and purges its old chunks when it replaces one, so the hand path is
granted the direct and message gets and the purge of each node's object stream; a test on a bus running the composed
user list walks the hand path end to end.
2026-10-24 18:25:46 +02:00

138 lines
6.0 KiB
Go

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-<node>`.
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.<node>.<module>`, `want.<node>.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.<seat>.tool.<verb>.<node>`.
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." + BackupHolderModule + ".event.backed-up"}
// BackupHolderModule is the module that holds node-backup on every node, restic: the one whose word a node-engine hears.
// A second holder of the seat is a change of this grant.
const BackupHolderModule = "restic"
// 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
}
// administers says a person may invoke every tool (`*`): the operator.
func administers(invokes []string) bool {
for _, i := range invokes {
if i == "*" {
return true
}
}
return false
}
// HandPathWrites is what the operator's account publishes for release 6's hand path (ADR 0539 clauses 2 and 10): a want
// of any node, written and read back, in the assigner's bucket; each named node's compositions; the digests; and each
// node's object bucket, for a file over 64 KiB.
func HandPathWrites(nodes []string) ([]string, error) {
out := append([]string{"$KV." + delivery.AssignerWantsBucket + ".want.>"}, readGrants(delivery.AssignerWantsBucket, "want.>")...)
out = append(out, "$KV."+ControllerComposedBucket+".>")
out = append(out, readGrants(ControllerComposedBucket, "")...)
for _, node := range nodes {
w, err := ComposedWrites(node)
if err != nil {
return nil, err
}
bucket, _ := composedBucketOf(node)
out = append(out, w)
out = append(out, readGrants(bucket, "")...)
files, err := DeclarationFilesWrites(node)
if err != nil {
return nil, err
}
stream := "OBJ_" + DeclarationFilesBucket + node
// The object store's put reads an object's metadata already there and, replacing it, purges its old chunks.
out = append(out, files, "$JS.API.STREAM.INFO."+stream, "$JS.API.DIRECT.GET."+stream+".>",
"$JS.API.STREAM.MSG.GET."+stream, "$JS.API.STREAM.PURGE."+stream)
}
return out, nil
}