146 lines
4.9 KiB
Go
146 lines
4.9 KiB
Go
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 (`${dir:mesh-state}/ledgers`). Its manifest declares the secret only once
|
|
// the vault holds it on every machine (B3 of issue 1966): until then a file not there means no ledger credentials, and
|
|
// every ledger write is refused naming the missing one.
|
|
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()
|
|
}
|
|
}
|