213 lines
12 KiB
Go
213 lines
12 KiB
Go
package broker
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// The provisioner client's own bus account (novox/hq ADR 0349 clauses 6 and 11, design 57 package P3).
|
|
//
|
|
// **Not the tool runner's account.** The client fills a machine's files and asks the vault for the secrets that
|
|
// machine's declaration names; the vault hands a secret only to an account of its own, because a call under the
|
|
// tool runner's account cannot be told from an agent's on that machine (ADR 0305 §3, §12). So each machine that
|
|
// runs the client has a principal of its own, KindProvisioner, named for the machine, and granted what the client
|
|
// does and nothing else: read its own machine's keys in each domain's bucket, write its own machine's keys of the
|
|
// provisioner server's needs, applied and collections (design 57 P4), read the server's registry, say what it
|
|
// applied on its own machine, and ask the vault in its own name.
|
|
|
|
// NodeProvisionerSeat is the seat the provisioner client holds on every machine (ADR 0349 clause 3).
|
|
const NodeProvisionerSeat = "node-provisioner"
|
|
|
|
// ProvisionerSecret is the own secret of the module running the client that its bus account is delivered in, as
|
|
// `broker` is a module's own (novox/hq issue 195): a module that claims NodeProvisionerSeat and declares no own
|
|
// secret of this name has nowhere to read an account, and is given none.
|
|
const ProvisionerSecret = "provisioner-broker"
|
|
|
|
// ConfigResolvedBucket is the config domain's resolved values, keyed `<node>.<module>.<key>` (ADR 0348, research
|
|
// 051): mesh-config's state `resolved`, written by mesh-config alone.
|
|
var ConfigResolvedBucket = BucketName("mesh-config", "resolved")
|
|
|
|
// ProvisionerReads are the domains' buckets a provisioner client reads its own machine's keys of, each keyed
|
|
// `<node>.…` (research 051): placement, the controller's assignments mirror (`<node>.<module>`), whose value carries
|
|
// the provisioner's plan of a module handed to it (inventory.Provisioning, design 57 P5); and config,
|
|
// mesh-config's resolved values. **Named one by one**, as the controller's asks are: a domain that moves to the
|
|
// provisioner adds its bucket here, and a bucket not named is one the client cannot read.
|
|
var ProvisionerReads = []string{ConfigResolvedBucket}
|
|
|
|
// ProvisionerReadsWhole are the buckets a provisioner client reads every key of: the controller's assignments mirror,
|
|
// where a provider's consumers are bound to it in their own machines' plans, which the provider machine's client
|
|
// reads to fill ${consumers:<provision>} with each one's credential (novox/hq ADR 0373, issue 679). It holds placement
|
|
// and plans, never a secret's value; the client writes none of it.
|
|
var ProvisionerReadsWhole = []string{AssignmentsBucket}
|
|
|
|
// BoundState is a providing module's state of entries, one per consumer (mesh-sdk go/provider, issue 678): a
|
|
// consumer's machine's client reads its own keys of it, `<node>.<module>.<provision>[.<local>]`, to fill
|
|
// ${bound:<provision>:<key>} (issue 679).
|
|
const BoundState = "bound"
|
|
|
|
// ProvisionerServerModule is the module holding the seat mesh-provisioner once for the mesh (ADR 0349 clause 3,
|
|
// design 57 P4), whose buckets the clients write their own machine's keys of.
|
|
const ProvisionerServerModule = "mesh-provisioner"
|
|
|
|
// The provisioner server's buckets (ADR 0349 clause 3): its state, created by the controller from its manifest
|
|
// like any module's, and written by its own account whole.
|
|
var (
|
|
// ProvisionerRegistryBucket is which domain answers which kind of placeholder, keyed by the kind; the
|
|
// server's alone, read whole by every client.
|
|
ProvisionerRegistryBucket = BucketName(ProvisionerServerModule, "registry")
|
|
// ProvisionerNeedsBucket is what each module on each machine needs, `<node>.<module>.<kind>.<name>`.
|
|
ProvisionerNeedsBucket = BucketName(ProvisionerServerModule, "needs")
|
|
// ProvisionerAppliedBucket is what is applied where, `<node>.<module>`.
|
|
ProvisionerAppliedBucket = BucketName(ProvisionerServerModule, "applied")
|
|
// ProvisionerCollectBucket is the items of every collection, `<node>.<collection>.<contributor>`.
|
|
ProvisionerCollectBucket = BucketName(ProvisionerServerModule, "collect")
|
|
)
|
|
|
|
// ProvisionerWrites are the server's buckets a client writes, and reads, its own machine's keys of (`<node>.>`)
|
|
// and no other's: the needs it records, what it applied, and the items of the collections on its machine. A
|
|
// client cannot write another machine's needs, or say another machine applied a file.
|
|
var ProvisionerWrites = []string{ProvisionerNeedsBucket, ProvisionerAppliedBucket, ProvisionerCollectBucket}
|
|
|
|
// VerbsTheProvisionerAsksTheVault are the vault's verbs a provisioner client asks in its own name (ADR 0349
|
|
// clause 11): `get`, a secret its machine's declaration names, answered sealed to the asker's key. On the subject
|
|
// that names its caller, so the vault knows which machine's client asks from the bus's word.
|
|
var VerbsTheProvisionerAsksTheVault = []SeatVerb{{Seat: "mesh-vault", Verb: "get"}}
|
|
|
|
// VaultSecretChangedSubject is where the vault says a secret it holds changed (`secret-changed`: which, by whom,
|
|
// when, its generation; never a value, ADR 0349 clause 11), as link.EventSubject addresses mesh-vault's event. A
|
|
// provisioner client listens to it so a rotated secret reaches its file without a new declaration (design 57 V4):
|
|
// it asks the vault again, in its own name, for a secret its machine's declaration names.
|
|
const VaultSecretChangedSubject = "mesh.mod.mesh-vault.event.secret-changed"
|
|
|
|
// ProvisionerAppliedSubject is where a machine's client says what it applied for one module, applied or failed
|
|
// with the reason (ADR 0349 clauses 4 and 15): `prov.applied.<node>.<module>`.
|
|
func ProvisionerAppliedSubject(node, module string) string {
|
|
return "prov.applied." + node + "." + module
|
|
}
|
|
|
|
// ProvisionerAskSubject is where a machine's tool runner asks that machine's provisioner client for a config value
|
|
// or a non-secret bound value of a module it carries, naming the module from the bundle that asks (novox/hq ADR
|
|
// 0370 §1, §3, §4; issue 685): `prov.ask.<node>`, which the client subscribes and the tool runner's account of the
|
|
// same machine alone may publish. The bus proves only that the tool runner asks; the module is the runtime's word,
|
|
// so the client answers no secret here (ADR 0370 §2).
|
|
func ProvisionerAskSubject(node string) string { return "prov.ask." + node }
|
|
|
|
// ProvisionerChangedSubject is where a machine's client says that a value of one module it answers moved, so the
|
|
// tool runner asks again for the module's code's watches (ADR 0349 §14, ADR 0370 §1): `prov.changed.<node>.<module>`,
|
|
// carrying which value and never the value itself, heard by that machine's tool runner alone.
|
|
func ProvisionerChangedSubject(node, module string) string {
|
|
return "prov.changed." + node + "." + module
|
|
}
|
|
|
|
// provisionerGrants is a provisioner client's whole authority: to publish, and to subscribe.
|
|
func provisionerGrants(p Principal) (pub, sub []string, err error) {
|
|
if p.Node == "" {
|
|
// Refused rather than composed into `$KV.<bucket>..>`, a pattern no machine's keys are under, and a
|
|
// name every nameless client would share.
|
|
return nil, nil, errors.New("a provisioner client names no machine, so it would have no keys of its own " +
|
|
"to read and an account it shared with every other")
|
|
}
|
|
for _, bucket := range ProvisionerReads {
|
|
pub = append(pub, machineKeysRead(bucket, p.Node)...)
|
|
}
|
|
for _, bucket := range ProvisionerReadsWhole {
|
|
pub = append(pub, wholeBucketRead(bucket)...)
|
|
}
|
|
for _, bucket := range p.BoundBuckets {
|
|
module, name, ok := strings.Cut(bucket, "_")
|
|
if !ok || name != BoundState || !safeSubject.MatchString(module) || strings.Contains(module, "_") {
|
|
return nil, nil, fmt.Errorf("%q is not a provider's bucket of entries, <module>_%s", bucket, BoundState)
|
|
}
|
|
pub = append(pub, machineKeysRead(bucket, p.Node)...)
|
|
}
|
|
// The server's buckets (design 57 P4): its own machine's keys of the needs, the applied and the
|
|
// collections, read and written, `$KV.<bucket>.<node>.>` and nothing wider; and the registry, read whole,
|
|
// which says which domain answers which kind and is the same for every machine.
|
|
for _, bucket := range ProvisionerWrites {
|
|
pub = append(pub, machineKeysRead(bucket, p.Node)...)
|
|
pub = append(pub, "$KV."+bucket+"."+p.Node+".>")
|
|
}
|
|
pub = append(pub, wholeBucketRead(ProvisionerRegistryBucket)...)
|
|
pub = append(pub, "prov.applied."+p.Node+".>")
|
|
token := CallerToken(p.Username())
|
|
if token == "" {
|
|
return nil, nil, fmt.Errorf("%q cannot name a caller", p.Username())
|
|
}
|
|
for _, v := range VerbsTheProvisionerAsksTheVault {
|
|
pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+token)
|
|
}
|
|
// What moved of a value it answers its own machine's tool runner (ADR 0370 §1), under its own machine's name.
|
|
pub = append(pub, "prov.changed."+p.Node+".>")
|
|
// Its own reply space, for the vault's answers and its reads' and watches' deliveries; what the vault says
|
|
// changed; and its own machine's tool runner asking it for a module's config and non-secret bound values (ADR
|
|
// 0370 §3), which it answers on the ask's reply and nowhere else.
|
|
return pub, []string{p.inbox(), VaultSecretChangedSubject, ProvisionerAskSubject(p.Node)}, nil
|
|
}
|
|
|
|
// machineKeysRead is reading every key of a bucket under one machine's name, `<node>.>`, and no other key: a
|
|
// direct get of a key's own subject, and a consumer whose filter is under it — the server's create-with-filter
|
|
// form, which it holds to the filter the subject names (as stateGrants does for one key, novox/hq ADR 0260).
|
|
// Binding, deleting a consumer and flow control cannot be narrowed by key and read no value.
|
|
func machineKeysRead(bucket, node string) []string {
|
|
stream := "KV_" + bucket
|
|
keys := "$KV." + bucket + "." + node + ".>"
|
|
return []string{
|
|
"$JS.API.STREAM.INFO." + stream,
|
|
"$JS.API.DIRECT.GET." + stream + "." + keys,
|
|
"$JS.API.CONSUMER.CREATE." + stream + ".*." + keys,
|
|
"$JS.API.CONSUMER.DELETE." + stream + ".>",
|
|
"$JS.FC." + stream + ".>",
|
|
}
|
|
}
|
|
|
|
// wholeBucketRead is reading every key of a bucket and writing none: what stateGrants grants a module for a
|
|
// bucket it `reads`.
|
|
func wholeBucketRead(bucket string) []string {
|
|
stream := "KV_" + bucket
|
|
return []string{
|
|
"$JS.API.STREAM.INFO." + stream,
|
|
"$JS.API.DIRECT.GET." + stream + ".>",
|
|
"$JS.API.CONSUMER.CREATE." + stream + ".>",
|
|
"$JS.API.CONSUMER.DELETE." + stream + ".>",
|
|
"$JS.FC." + stream + ".>",
|
|
}
|
|
}
|
|
|
|
// LastProvApplied is what a machine's provisioner client last said of one module's files, as it published it on
|
|
// ProvisionerAppliedSubject and ProvAppliedStream keeps it (design 57 P6): false when it has said nothing of it in
|
|
// the stream's week.
|
|
func LastProvApplied(js nats.JetStreamContext, node, module string) ([]byte, bool, error) {
|
|
msg, err := js.GetLastMsg(ProvAppliedStream, ProvisionerAppliedSubject(node, module))
|
|
if errors.Is(err, nats.ErrMsgNotFound) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("what %s's provisioner client said of %s cannot be read: %w", node, module, err)
|
|
}
|
|
return msg.Data, true, nil
|
|
}
|
|
|
|
// ProvAppliedReports is the newest word of every machine's provisioner client on every module it said one of, kept by
|
|
// ProvAppliedStream, by subject (`prov.applied.<node>.<module>`).
|
|
func ProvAppliedReports(js nats.JetStreamContext) (map[string][]byte, error) {
|
|
info, err := js.StreamInfo(ProvAppliedStream, &nats.StreamInfoRequest{SubjectsFilter: ProvAppliedSubjects})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the provisioner clients' reports cannot be listed: %w", err)
|
|
}
|
|
out := map[string][]byte{}
|
|
for subject := range info.State.Subjects {
|
|
msg, err := js.GetLastMsg(ProvAppliedStream, subject)
|
|
if errors.Is(err, nats.ErrMsgNotFound) {
|
|
continue // aged out between the two reads
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the provisioner client's report on %s cannot be read: %w", subject, err)
|
|
}
|
|
out[subject] = msg.Data
|
|
}
|
|
return out, nil
|
|
}
|