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() } }