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
99 lines
4.2 KiB
Go
99 lines
4.2 KiB
Go
package bus
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// A module's code asks its machine's provisioner client for its values at runtime (novox/hq ADR 0349 clause 14 as
|
|
// ADR 0370 changes it, issue 685).
|
|
//
|
|
// While a module's code runs here, its asks reach the bus under this runtime's account, and the bus proves only
|
|
// that account. So the runtime asks its own machine's client on ProvisionAskSubject — which the controller grants
|
|
// this machine's tool runner alone to publish (ADR 0370 §3) — naming the module itself, from the bundle that asks
|
|
// and never from the module's code or arguments (§4). The client answers that module's config values, for what its
|
|
// manifest declares, and no secret: the vault hands a secret only to a caller the bus proves (§2).
|
|
|
|
// ProvisionAskSubject is where this machine's tool runner asks its provisioner client (mesh-controller
|
|
// internal/broker ProvisionerAskSubject): `prov.ask.<node>`.
|
|
func ProvisionAskSubject(node string) string { return "prov.ask." + node }
|
|
|
|
// ProvisionChangedSubjects is every module's word of what moved on a machine (mesh-controller internal/broker
|
|
// ProvisionerChangedSubject): `prov.changed.<node>.<module>`, a module's name dots and all.
|
|
func ProvisionChangedSubjects(node string) string { return "prov.changed." + node + ".>" }
|
|
|
|
// ProvisionTimeout bounds one ask of the client; a variable so a test need not wait.
|
|
var ProvisionTimeout = 10 * time.Second
|
|
|
|
// ProvisionChanged is what the client says moved of one module: the kind, and the key, or every key of the kind when
|
|
// empty. Never the value.
|
|
type ProvisionChanged struct {
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key,omitempty"`
|
|
}
|
|
|
|
// ProvisionRefused is the client's refusal, in its own words: a key the module does not declare, a module not placed
|
|
// on the machine, a secret, a kind it does not answer yet.
|
|
type ProvisionRefused struct{ Reason string }
|
|
|
|
func (r *ProvisionRefused) Error() string { return r.Reason }
|
|
|
|
// AskProvisioner asks this machine's provisioner client for one value of module: kind `config` or `bound`, and the
|
|
// key. The module is the caller's to name from the bundle it runs; this never takes it from what a bundle sent.
|
|
func (c *Conn) AskProvisioner(module, kind, key string) (json.RawMessage, error) {
|
|
if c.node == "" {
|
|
return nil, errors.New("this runtime names no machine, so it has no provisioner client to ask")
|
|
}
|
|
body, err := json.Marshal(map[string]string{"module": module, "kind": kind, "key": key})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msg, err := c.nc.Request(ProvisionAskSubject(c.node), body, ProvisionTimeout)
|
|
if errors.Is(err, nats.ErrNoResponders) {
|
|
return nil, fmt.Errorf("%s's provisioner client does not answer: it is not running there, or the bus does "+
|
|
"not let %s ask it", c.node, c.cred.User)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("asking %s's provisioner client for %s's %s %s: %w", c.node, module, kind, key, err)
|
|
}
|
|
var answered struct {
|
|
Value json.RawMessage `json:"value"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(msg.Data, &answered); err != nil {
|
|
return nil, fmt.Errorf("%s's provisioner client answered something that is not its answer: %w", c.node, err)
|
|
}
|
|
if answered.Error != "" {
|
|
return nil, &ProvisionRefused{Reason: answered.Error}
|
|
}
|
|
if len(answered.Value) == 0 {
|
|
return nil, fmt.Errorf("%s's provisioner client answered no value and no reason", c.node)
|
|
}
|
|
return answered.Value, nil
|
|
}
|
|
|
|
// OnProvisionChanged hands told every word of this machine's provisioner client of what moved, by module, until
|
|
// stopped.
|
|
func (c *Conn) OnProvisionChanged(told func(module string, changed ProvisionChanged)) (func(), error) {
|
|
if c.node == "" {
|
|
return nil, errors.New("this runtime names no machine, so it hears no provisioner client")
|
|
}
|
|
prefix := "prov.changed." + c.node + "."
|
|
sub, err := c.nc.Subscribe(ProvisionChangedSubjects(c.node), func(m *nats.Msg) {
|
|
var changed ProvisionChanged
|
|
if json.Unmarshal(m.Data, &changed) != nil || changed.Kind == "" {
|
|
return
|
|
}
|
|
told(strings.TrimPrefix(m.Subject, prefix), changed)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() { _ = sub.Unsubscribe() }, c.nc.Flush()
|
|
}
|