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.`. 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..`, 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() }