mesh/merge-gate head 33d136e70227 merged into trunk 6b3e15042fee — pass, with a note: builds mesh-tools, node-tools (+40 dependent(s)) → ace, g14, novox, s…
mesh/repo-check head 33d136e70227 merged into trunk 6b3e15042fee — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
217 lines
6.8 KiB
Go
217 lines
6.8 KiB
Go
package runtime
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
)
|
|
|
|
// A module's code reads its values at runtime (novox/hq ADR 0349 clause 14 as ADR 0370 changes it, issue 685).
|
|
//
|
|
// A bundle asks `mesh/provision.get` and `mesh/provision.watch` with `{kind, key}`; the runtime asks its machine's
|
|
// provisioner client **as the module whose bundle asks** — the module this bus was made for, from the bundle it
|
|
// launched, never a name the bundle sent (ADR 0370 §4) — and hands the answer back. A watch hands the current value
|
|
// first and then each change: the client says which value moved, and the runtime asks again. A secret is refused
|
|
// here, with the reason, and never asked: the client serves no secret on the tool runner's word (§2), and a module
|
|
// under an account of its own asks the vault itself, which waits on issue 569.
|
|
|
|
// The kinds a bundle may ask.
|
|
const (
|
|
provisionConfig = "config"
|
|
provisionBound = "bound"
|
|
provisionSecret = "secret"
|
|
)
|
|
|
|
// provisionAsked is what a bundle names. Anything else it sends, a module's name among it, is not read.
|
|
type provisionAsked struct {
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
// ProvisionChange is what a watch hands a bundle: the value, or, when it is no longer answered, why.
|
|
type ProvisionChange struct {
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
// Refused is why the value is no longer answered: the module taken off the machine, the key no longer declared.
|
|
Refused string `json:"refused,omitempty"`
|
|
}
|
|
|
|
func readProvisionAsked(verb string, params json.RawMessage) (provisionAsked, error) {
|
|
var asked provisionAsked
|
|
if err := json.Unmarshal(params, &asked); err != nil || asked.Kind == "" || asked.Key == "" {
|
|
return asked, fmt.Errorf("mesh/provision.%s names no value: {kind, key}", verb)
|
|
}
|
|
switch asked.Kind {
|
|
case provisionConfig, provisionBound:
|
|
return asked, nil
|
|
case provisionSecret:
|
|
return asked, fmt.Errorf("the secret %s is not asked for at runtime while the module's code runs in the "+
|
|
"tool runner: the vault hands a secret only to a caller the bus proves, and the bus proves only the tool "+
|
|
"runner here (novox/hq ADR 0370 §2, ADR 0349 §11); the module's secrets reach it through its files, "+
|
|
"until it runs under a bus account of its own (issue 569)", asked.Key)
|
|
}
|
|
return asked, fmt.Errorf("mesh/provision.%s: %q is not a kind of value a module reads at runtime: config or bound",
|
|
verb, asked.Kind)
|
|
}
|
|
|
|
// Provision answers a bundle's `get` of one of its module's values.
|
|
func (b *moduleBus) Provision(verb string, params json.RawMessage) (json.RawMessage, error) {
|
|
if verb != "get" {
|
|
return nil, fmt.Errorf("the runtime answers no mesh/provision.%s", verb)
|
|
}
|
|
asked, err := readProvisionAsked(verb, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.all.conn.AskProvisioner(b.module, asked.Kind, asked.Key)
|
|
}
|
|
|
|
// ProvisionWatch hands deliver the value now and each change after, until stopped. A value not answered now is
|
|
// refused now, with the client's reason.
|
|
func (b *moduleBus) ProvisionWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
|
|
asked, err := readProvisionAsked("watch", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
value, err := b.all.conn.AskProvisioner(b.module, asked.Kind, asked.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
w := &provisionWatch{bus: b, asked: asked, deliver: deliver, last: value}
|
|
if err := w.hand(ProvisionChange{Kind: asked.Kind, Key: asked.Key, Value: value}); err != nil {
|
|
return nil, err
|
|
}
|
|
stop, err := b.all.provisioned.add(b.all, b.module, w)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return stop, nil
|
|
}
|
|
|
|
// provisionWatch is one watch a bundle holds: what it asked, and what it was handed last.
|
|
type provisionWatch struct {
|
|
bus *moduleBus
|
|
asked provisionAsked
|
|
deliver func(json.RawMessage) error
|
|
mu sync.Mutex // one change at a time, in order
|
|
last json.RawMessage
|
|
refused string
|
|
stopped bool
|
|
}
|
|
|
|
func (w *provisionWatch) hand(change ProvisionChange) error {
|
|
raw, err := json.Marshal(change)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.deliver(raw)
|
|
}
|
|
|
|
// again asks once more after the client said something moved, and hands the bundle what changed.
|
|
func (w *provisionWatch) again() {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.stopped {
|
|
return
|
|
}
|
|
value, err := w.bus.all.conn.AskProvisioner(w.bus.module, w.asked.Kind, w.asked.Key)
|
|
change := ProvisionChange{Kind: w.asked.Kind, Key: w.asked.Key}
|
|
var refused *bus.ProvisionRefused
|
|
switch {
|
|
case errors.As(err, &refused):
|
|
if w.refused == refused.Reason {
|
|
return
|
|
}
|
|
change.Refused = refused.Reason
|
|
case err != nil:
|
|
// The client not reached: asked again on its next word, which it says whenever it comes up.
|
|
w.bus.all.logf("[mesh-tools] %s's watch of its %s %s could not ask again: %v", w.bus.module, w.asked.Kind,
|
|
w.asked.Key, err)
|
|
return
|
|
default:
|
|
if w.refused == "" && bytes.Equal(value, w.last) {
|
|
return
|
|
}
|
|
change.Value = value
|
|
}
|
|
if err := w.hand(change); err != nil {
|
|
w.bus.all.logf("[mesh-tools] %s did not take a change of its %s %s: %v", w.bus.module, w.asked.Kind,
|
|
w.asked.Key, err)
|
|
return
|
|
}
|
|
w.last, w.refused = value, change.Refused
|
|
}
|
|
|
|
// provisionWatches is every watch on this runtime, by module, and the one subscription that hears the client.
|
|
type provisionWatches struct {
|
|
mu sync.Mutex
|
|
of map[string]map[*provisionWatch]bool
|
|
stop func()
|
|
}
|
|
|
|
func (p *provisionWatches) add(all *consumers, module string, w *provisionWatch) (func(), error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.stop == nil {
|
|
stop, err := all.conn.OnProvisionChanged(func(module string, changed bus.ProvisionChanged) {
|
|
for _, w := range p.told(module, changed) {
|
|
go w.again()
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the runtime cannot hear its machine's provisioner client: %w", err)
|
|
}
|
|
p.stop = stop
|
|
}
|
|
if p.of == nil {
|
|
p.of = map[string]map[*provisionWatch]bool{}
|
|
}
|
|
if p.of[module] == nil {
|
|
p.of[module] = map[*provisionWatch]bool{}
|
|
}
|
|
p.of[module][w] = true
|
|
return func() {
|
|
w.mu.Lock()
|
|
w.stopped = true
|
|
w.mu.Unlock()
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
delete(p.of[module], w)
|
|
}, nil
|
|
}
|
|
|
|
// told is the watches of module that what moved concerns.
|
|
func (p *provisionWatches) told(module string, changed bus.ProvisionChanged) []*provisionWatch {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
var out []*provisionWatch
|
|
for w := range p.of[module] {
|
|
if w.asked.Kind == changed.Kind && (changed.Key == "" || changed.Key == w.asked.Key) {
|
|
out = append(out, w)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (p *provisionWatches) stopAll() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
for _, ws := range p.of {
|
|
for w := range ws {
|
|
w.mu.Lock()
|
|
w.stopped = true
|
|
w.mu.Unlock()
|
|
}
|
|
}
|
|
p.of = nil
|
|
if p.stop != nil {
|
|
p.stop()
|
|
p.stop = nil
|
|
}
|
|
}
|