Publish a module's ledger history and snapshots only on its own ledger connection, read from the runner's own secret ledgers and followed without a restart (issue 1966 T6)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
)
|
||||
|
||||
// The ledger credentials (novox/hq ADR 0597 §9, design 25 § 12.4; issue 1966 T6): one per module assignment, held by
|
||||
// the machine's tool runner in its own secret `ledgers`, a map of module to credential (`{}` when none), which the vault
|
||||
// rewrites on each mint and retirement. **A module's ledger history is appended, and its snapshots written, only on
|
||||
// that module's own ledger connection**; everything else stays on the runner's own.
|
||||
|
||||
// LedgersFile is where the runner reads its own secret `ledgers`: MESH_LEDGERS_FILE when set, else beside its broker
|
||||
// credential, both in its directory mesh-state (node-tools' manifest: `${dir:mesh-state}/ledgers`).
|
||||
const LedgersFile = "MESH_LEDGERS_FILE"
|
||||
|
||||
// LedgersFileOf is the path of the secret `ledgers` for a runtime whose broker credential is at broker.
|
||||
func LedgersFileOf(broker string) string {
|
||||
if file := os.Getenv(LedgersFile); file != "" {
|
||||
return file
|
||||
}
|
||||
if broker == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(filepath.Dir(broker), "ledgers")
|
||||
}
|
||||
|
||||
// LedgerCredentials are the ledger connections of the modules on this machine, read from the secret `ledgers` and
|
||||
// followed as it changes, with no restart.
|
||||
type LedgerCredentials struct {
|
||||
file string
|
||||
dial func(bus.Credential) (*bus.Conn, error)
|
||||
logf func(string, ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
creds map[string]bus.Credential
|
||||
conns map[string]*bus.Conn
|
||||
why string // why the file could not be read, when it could not
|
||||
stop func()
|
||||
}
|
||||
|
||||
// FollowLedgerCredentials reads the secret `ledgers` at file and follows it (WatchFile), dialling a module's connection
|
||||
// with dial the first time it is asked for.
|
||||
func FollowLedgerCredentials(file string, dial func(bus.Credential) (*bus.Conn, error), logf func(string, ...any)) *LedgerCredentials {
|
||||
l := &LedgerCredentials{file: file, dial: dial, logf: logf, creds: map[string]bus.Credential{}, conns: map[string]*bus.Conn{}}
|
||||
l.read()
|
||||
stop, err := WatchFile(file, l.read, logf, "a change of the ledger credentials is taken only after a restart")
|
||||
if err != nil {
|
||||
logf("[mesh-tools] cannot follow the ledger credentials at %s: %v; a change is taken only after a restart", file, err)
|
||||
} else {
|
||||
l.stop = stop
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// read takes the file as it is now: a module whose credential changed or went has its connection closed, and is
|
||||
// dialled again from the new one when next asked for.
|
||||
func (l *LedgerCredentials) read() {
|
||||
creds, why := map[string]bus.Credential{}, ""
|
||||
raw, err := os.ReadFile(l.file)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
why = "the file " + l.file + " is not there yet"
|
||||
case err != nil:
|
||||
why = err.Error()
|
||||
case len(bytes.TrimSpace(raw)) == 0:
|
||||
why = "the file " + l.file + " is empty"
|
||||
default:
|
||||
if err := json.Unmarshal(raw, &creds); err != nil {
|
||||
why = "the file " + l.file + " is not a map of module to credential: " + err.Error()
|
||||
creds = map[string]bus.Credential{}
|
||||
}
|
||||
}
|
||||
l.mu.Lock()
|
||||
var closing []*bus.Conn
|
||||
for module, conn := range l.conns {
|
||||
if now, has := creds[module]; !has || !reflect.DeepEqual(now, l.creds[module]) {
|
||||
closing = append(closing, conn)
|
||||
delete(l.conns, module)
|
||||
}
|
||||
}
|
||||
saidBefore := l.why
|
||||
l.creds, l.why = creds, why
|
||||
l.mu.Unlock()
|
||||
for _, c := range closing {
|
||||
c.Close()
|
||||
}
|
||||
if why != "" && why != saidBefore {
|
||||
l.logf("[mesh-tools] the ledger credentials cannot be read (%s); no ledger is written until they can", why)
|
||||
}
|
||||
}
|
||||
|
||||
// Writer is a module's ledger connection, dialled from its credential the first time it is asked for; refused, naming
|
||||
// what is missing, when the secret holds none for it.
|
||||
func (l *LedgerCredentials) Writer(module string) (*bus.Conn, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if c := l.conns[module]; c != nil {
|
||||
return c, nil
|
||||
}
|
||||
cred, has := l.creds[module]
|
||||
if !has {
|
||||
why := ""
|
||||
if l.why != "" {
|
||||
why = " (" + l.why + ")"
|
||||
}
|
||||
return nil, fmt.Errorf("%s's ledger credential is missing from the tool runner's own secret ledgers%s: its history "+
|
||||
"is published only on its own ledger connection, which the grants part issues with its membership and the "+
|
||||
"vault writes there (novox/hq ADR 0597 §9)", module, why)
|
||||
}
|
||||
if cred.Module == "" {
|
||||
cred.Module = module
|
||||
}
|
||||
c, err := l.dial(cred)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s's ledger connection could not be opened: %w", module, err)
|
||||
}
|
||||
c.Logf = l.logf
|
||||
l.conns[module] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Stop ends the following and closes every ledger connection.
|
||||
func (l *LedgerCredentials) Stop() {
|
||||
if l.stop != nil {
|
||||
l.stop()
|
||||
}
|
||||
l.mu.Lock()
|
||||
conns := l.conns
|
||||
l.conns = map[string]*bus.Conn{}
|
||||
l.mu.Unlock()
|
||||
for _, c := range conns {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user