mesh/delivery merged: source.main moved to novox/mesh-delivery@c1b4533201c8
mesh/delivery-group group revert/774-l3 merged: every member is merged
mesh/check being carried to 6771db22ae69 on d8274204fbed | from mesh-catalog 1555241393fe | novox/mesh-delivery#109 in group revert/774-l3
L3 lands again with two fixes: a held module is never removed, and the grants cover L3's new subjects.
158 lines
7.2 KiB
Go
158 lines
7.2 KiB
Go
// Command mesh-grants is the grants part (novox/hq ADR 0547 clauses 5 to 8, issue 1945): a process on its own bus
|
|
// credential that watches placement, the assigner's wants, the indexer's manifests, the builder's builds, mesh-config's
|
|
// resolved values and the node agents' haves, and on every change composes each assignment's membership, the bus's user
|
|
// list and each manifest's buckets, logs and streams with the bus composer the controller uses (mesh-sdk buscompose).
|
|
//
|
|
// **Memberships are published for real** (the operator, 2026-10-25 22:31): since release 6 nobody publishes them, so a
|
|
// module placed since cannot serve its seat or reach a new state. Each is published on the assignments stream only when
|
|
// it differs from the one last published there. **A bucket, log or stream a manifest declares that the bus lacks is
|
|
// created** (the operator, 2026-10-26 08:54; issue 1790), once per manifest, with the manifest's limits, on the event
|
|
// that brought the manifest; one the bus holds is never changed. **The user list is written sealed to the bus node**
|
|
// (ADR 0547 clause 7) as an object of the part's own bucket named in `user-list.sealed`, which the bus node follows only
|
|
// from take-over. **The rest of the assertions and the grants stay in shadow** until
|
|
// the operator's take-over, asked on their channel and run on their yes after its refusals: they are composed, compared with what the bus loaded
|
|
// and holds (through the bus module's own read tools) and kept in the part's own state, and take no effect.
|
|
//
|
|
// It holds no timer, and never exits for a refused read, write or publish: a refusal is said and tried again at the next
|
|
// event.
|
|
//
|
|
// **No seat in this milestone.** ADR 0547 clause 5 gives the part the seat `mesh-grants`; a `mesh-*` seat must be in
|
|
// mesh-sdk's own seat list, which the running controller's composer does not know, so the part serves module tools
|
|
// (grants_*) and claims no seat until take-over. The composer grants the membership publish by the module's name,
|
|
// mesh-grants.
|
|
//
|
|
// Started by the tool runner, with no credential of its own, it serves those tools (a process never serves, ADR 0436).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/announce"
|
|
"git.novox.be/novox/mesh-sdk/go/asker"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
|
|
"mesh-grants/internal/grants"
|
|
)
|
|
|
|
func main() {
|
|
if os.Getenv("MESH_BROKER_FILE") == "" {
|
|
if err := serveTools(); err != nil {
|
|
fmt.Fprintln(os.Stderr, "mesh-grants:", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := run(ctx, os.Getenv, os.Stderr); err != nil {
|
|
fmt.Fprintln(os.Stderr, "mesh-grants:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// keptStates is what the part keeps, each readable (ADR 0495 §2-§3): the view and a session read them.
|
|
func keptStates() []announce.State {
|
|
var out []announce.State
|
|
for _, s := range grants.States {
|
|
out = append(out, announce.State{Name: s, Readable: true})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// run is the process: it returns when ctx ends, or when it has no credential or cannot dial the bus at all. A bucket or
|
|
// read the bus refuses never ends it.
|
|
func run(ctx context.Context, getenv func(string) string, logOut io.Writer) error {
|
|
logger := log.New(logOut, "mesh-grants: ", log.LstdFlags)
|
|
path := strings.TrimSpace(getenv("MESH_BROKER_FILE"))
|
|
if path == "" {
|
|
return errors.New("MESH_BROKER_FILE is not set, so there is no credential to read the bus with")
|
|
}
|
|
account, err := process.ReadAccount(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f := &grants.Follower{Logf: logger.Printf}
|
|
conn, err := process.Connect(account, nats.Name(account.Node+"."+account.Module), nats.RetryOnFailedConnect(true),
|
|
nats.ReconnectHandler(func(*nats.Conn) { f.Wake() }))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
if err := conn.Keeps(keptStates()...); err != nil {
|
|
logger.Printf("what the part keeps could not be said on the bus, so its readers are not granted until it is: %v", err)
|
|
}
|
|
// The reads of other parts' state, on a connection of the same account: a read the bus refuses is said and composed
|
|
// past (busReader), never the end of the process.
|
|
opts := []nats.Option{
|
|
nats.Name(account.Node + "." + account.Module + ".reads"),
|
|
nats.UserInfo(account.User, account.Password),
|
|
nats.CustomInboxPrefix(account.InboxPrefix()),
|
|
nats.MaxReconnects(-1),
|
|
nats.RetryOnFailedConnect(true),
|
|
nats.ReconnectHandler(func(*nats.Conn) { f.Wake() }),
|
|
}
|
|
if strings.TrimSpace(account.Fingerprint) != "" {
|
|
opts = append(opts, nats.Secure(process.PinnedToFingerprint(account.Fingerprint)))
|
|
}
|
|
nc, err := nats.Connect(account.URL, opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("connecting to the bus: %w", err)
|
|
}
|
|
defer nc.Close()
|
|
js, err := jetstream.New(nc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bus := newBusReader(ctx, js)
|
|
f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn},
|
|
Publish: memberships{ctx: ctx, nc: nc, js: js}, Create: creating{ctx: ctx, js: js},
|
|
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf}
|
|
f.Watch = bus
|
|
// The operator's word on take-over and hand-back is their yes on their channel: the part asks it itself, and acts on
|
|
// the warrant once (mesh-sdk asker), after its refusals.
|
|
client := &asker.Client{Module: account.Module, State: conn.State(grants.StateTakeover), Publish: publishVia(ctx, js),
|
|
Record: recordVia(ctx, nc, account.Module), Subscribe: decidedVia(nc, account.Module, logger.Printf)}
|
|
f.Part.Asker, f.Part.Say = client, sayVia(ctx, js, account.Module)
|
|
f.Part.Forget = func(id string) error { return conn.State(grants.StateTakeover).Delete("ask." + id) }
|
|
if err := client.Listen(f.Part.Warranted, f.Part.Said); err != nil {
|
|
logger.Printf("the operator's answers cannot be heard yet, so take-over and hand-back wait: %v", err)
|
|
}
|
|
f.Asked = func(wake func()) error {
|
|
return conn.State(grants.StateUserList).Watch(grants.KeyAsked, func(c stdio.StateChange) error {
|
|
if !c.Current {
|
|
wake()
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
// The controller makes this module's durable consumer on EVENTS from the manifest's consumes (filtered on the two
|
|
// condition subjects); it is pulled and every message acknowledged, so its pending never grows. Where it is not there
|
|
// yet the pass says so and hears again at the next change.
|
|
f.Hear = func(handle func(string, []byte)) error {
|
|
cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module)
|
|
if err != nil {
|
|
return fmt.Errorf("the consumer %s_%s on EVENTS: %w", account.Node, account.Module, err)
|
|
}
|
|
_, err = cons.Consume(func(m jetstream.Msg) {
|
|
handle(m.Subject(), m.Data())
|
|
_ = m.Ack()
|
|
}, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) }))
|
|
return err
|
|
}
|
|
logger.Printf("started on the account %s: publishing memberships and creating the declared buckets, logs and streams the bus lacks; and writing the user list sealed to the bus node, which the bus node follows from take-over; grants and any change to a held stream in shadow", account.User)
|
|
f.Run(ctx)
|
|
return nil
|
|
}
|