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 `..` (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 // `.…` (research 051): placement, the controller's assignments mirror (`.`), 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:} 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, `..[.]`, to fill // ${bound::} (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, `...`. ProvisionerNeedsBucket = BucketName(ProvisionerServerModule, "needs") // ProvisionerAppliedBucket is what is applied where, `.`. ProvisionerAppliedBucket = BucketName(ProvisionerServerModule, "applied") // ProvisionerCollectBucket is the items of every collection, `..`. ProvisionerCollectBucket = BucketName(ProvisionerServerModule, "collect") ) // ProvisionerWrites are the server's buckets a client writes, and reads, its own machine's keys of (`.>`) // 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..`. 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.`, 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..`, // 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...>`, 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, _%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...>` 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, `.>`, 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..`). 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 }