Files
mesh-tools/node-tools/internal/runtime/ledger_credentials.go
T

144 lines
4.7 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 (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()
}
}