mesh/merge-gate head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] t…
mesh/repo-check head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] i…
mesh/delivery delivered
Providers, the state bus, templates and the environment collection that the feature's modules build on (feature 57, config and provisioning).
636 lines
23 KiB
Go
636 lines
23 KiB
Go
// Package provider is a providing module's half of a binding: it answers the provider's consumers itself, over the
|
|
// bus (novox/hq ADR 0349 clause 12, design 57 B1, issue 678).
|
|
//
|
|
// **Who binds the provider.** Each node's provisioner client records the needs of the modules handed to it from
|
|
// their plans, and the controller names in each plan, per required provision and local name, the provider that
|
|
// resolution chose: `provisioning.provisions` in `<node>.<module>` of the assignments mirror (issue 760). A recorded
|
|
// need names no provider (issue 677's note 7), so the plan's entry is where a bind is seen, as the vault sees it
|
|
// (mesh-vault pair.go). A consumer binds this provider when an entry names this machine and this module, and
|
|
// unbinds when the entry, or the assignment, goes. A consumer still filled the old way has no plan and is not seen
|
|
// here; the controller answers it as before (ADR 0349 clause 17).
|
|
//
|
|
// **One entry per consumer, in the provider's own bucket.** For each bind the helper composes the consumer's
|
|
// served values from the provider's offer (its config already composed into it, ADR 0202 §7) and the consumer's
|
|
// identity (`${consumer:as}`, `${consumer:as:dns}`, ADR 0202 §1), and writes one entry, `<node>.<module>.
|
|
// <provision>[.<local>]` of `<provider module>_bound`: the one statement both ends read. It never carries a
|
|
// secret. It names the credential the vault keeps for the pair, which the consumer's client asks the vault for.
|
|
//
|
|
// **The credential is the vault's** (ADR 0349 clause 8). The helper mints nothing and reads nothing of the
|
|
// provider's own secrets: it reads the value from the vault and hands it to the provider's adapter, which makes the
|
|
// login its software keeps. An offer whose software holds one credential (ADR 0158) is the vault's one value per
|
|
// provider and provision, `provision/<provider node>/<provision>`, with the provider's one user name; an offer with
|
|
// a login per consumer (ADR 0048, ADR 0158 §11, the default) is each consumer's own value,
|
|
// `pair/<provider node>/<provision>/<node>/<module>[/<local>]`, with the consumer's identity as its login (issue
|
|
// 677's notes 12 and 13). A credential the vault does not hold yet is waited for: nothing is made and no entry is
|
|
// written, so a consumer is never told of a login its provider has not made. A provider in the tool runner never asks
|
|
// the vault itself: its Vault is ConsumersFile, the file its machine's provisioner client fills (ADR 0373).
|
|
//
|
|
// **A multi-holder consumer gets no derived value** (ADR 0202 §10 as ADR 0349 clause 12 changes it): a consumer
|
|
// bound under a local name keeps several holders of the provision, each with its own login, while what the
|
|
// provider derives names one; its entry refuses it, naming both ends, and its client reports it.
|
|
//
|
|
// The helper speaks to the bus and the vault through the two interfaces below, so a provider wires it to its
|
|
// runtime and a test to a stand-in. Standard library only.
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// AssignmentsBucket is the controller's assignments mirror, `<node>.<module>`, whose value carries the plan of a
|
|
// module handed to the provisioner.
|
|
const AssignmentsBucket = "mesh-controller_assignments"
|
|
|
|
// BucketOf is a providing module's own bucket of entries, one per consumer: `<module>_bound`, the module's state
|
|
// `bound`.
|
|
func BucketOf(module string) string { return module + "_bound" }
|
|
|
|
// Bus is what the helper needs of the bus: a key-value watch and the provider's writes to its own bucket.
|
|
type Bus interface {
|
|
// Watch hands handle every current key and value of bucket, then each change as it comes, a removed key with a
|
|
// nil value; it returns once the current values are handed, and the watch lasts until ctx ends.
|
|
Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error
|
|
Put(ctx context.Context, bucket, key string, value []byte) error
|
|
Delete(ctx context.Context, bucket, key string) error
|
|
}
|
|
|
|
// Vault is how the provider reads a credential the vault keeps, by its name, opened.
|
|
type Vault interface {
|
|
Value(ctx context.Context, name string) ([]byte, error)
|
|
}
|
|
|
|
// CredentialForm is which credential an offer's consumers present.
|
|
type CredentialForm int
|
|
|
|
const (
|
|
// PerConsumer is a login of each consumer's own (ADR 0048, ADR 0158 §11): the default.
|
|
PerConsumer CredentialForm = iota
|
|
// Shared is the one credential the provider's software holds, shared with every consumer (ADR 0158).
|
|
Shared
|
|
// NoCredential is an offer whose consumers present none.
|
|
NoCredential
|
|
)
|
|
|
|
// Offer is what the provider serves one provision with.
|
|
type Offer struct {
|
|
// Serves are the values served to each consumer, the provider's config already composed into them; a string
|
|
// may name the consumer's identity, `${consumer:as}` or `${consumer:as:dns}`.
|
|
Serves map[string]any
|
|
// Credential is the form of the credential its consumers present.
|
|
Credential CredentialForm
|
|
// User is the provider's one user name, for a Shared credential.
|
|
User string
|
|
}
|
|
|
|
// Entry is one consumer's entry in the provider's bucket: the one statement both ends read. Never a secret.
|
|
type Entry struct {
|
|
Provider string `json:"provider"`
|
|
ProviderModule string `json:"provider-module"`
|
|
Provision string `json:"provision"`
|
|
Consumer string `json:"consumer"`
|
|
Module string `json:"module"`
|
|
Local string `json:"local,omitempty"`
|
|
// As is the consumer's identity, the mesh's (ADR 0202 §1).
|
|
As string `json:"as,omitempty"`
|
|
// User is the login the consumer presents: its identity, or the provider's one user name for a shared credential.
|
|
User string `json:"user,omitempty"`
|
|
// Values are the served values, composed for this consumer: what `${bound:<provision>:<key>}` reads.
|
|
Values map[string]any `json:"values,omitempty"`
|
|
// Credential is the name of the credential in the vault, which the consumer's client asks the vault for.
|
|
Credential string `json:"credential,omitempty"`
|
|
// Refused, when set, is why the consumer is not served, naming both ends.
|
|
Refused string `json:"refused,omitempty"`
|
|
// Contributes is what the consumer's module tells its provider about this provision (a database's name, its
|
|
// extensions), from its plan's entry (issue 776). Handed to Make and Gone; never written to the bucket.
|
|
Contributes map[string]any `json:"-"`
|
|
// At and From are where the consumer's plan says the provider is and which machine it is (issue 788): the values
|
|
// today's binding file gives that consumer. Written into Values as `at` and `from`; never alone to the bucket.
|
|
At, From string `json:"-"`
|
|
// Names are the names the controller composed for this consumer on this provision (issue 792): its own name, its
|
|
// internal name, and one of each per local name it contributes under, keyed as its binding file names them.
|
|
// Written into Values over anything the provider serves under the key; never alone to the bucket.
|
|
Names map[string]string `json:"-"`
|
|
}
|
|
|
|
// Key is the entry's key in the provider's bucket: `<node>.<module>.<provision>`, and `.<local>` for a bind under a
|
|
// local name.
|
|
func (e Entry) Key() string {
|
|
k := e.Consumer + "." + e.Module + "." + e.Provision
|
|
if e.Local != "" {
|
|
k += "." + e.Local
|
|
}
|
|
return k
|
|
}
|
|
|
|
// Consumer is what the provider's adapter makes: the consumer's entry and the credential the vault keeps for it.
|
|
type Consumer struct {
|
|
Entry Entry
|
|
Credential []byte
|
|
}
|
|
|
|
// Options are the provider's.
|
|
type Options struct {
|
|
// Node and Module are this provider: the machine it runs on and the module serving.
|
|
Node, Module string
|
|
// Offers are the provisions it serves, by name.
|
|
Offers map[string]Offer
|
|
Bus Bus
|
|
Vault Vault
|
|
// Make brings a consumer into being in the provider's software under the vault's credential. Asked on every
|
|
// pass for every consumer answered, so it must leave one that is already as asked alone; a rotated credential
|
|
// reaches the software this way.
|
|
Make func(ctx context.Context, c Consumer) error
|
|
// Gone, when set, hears that a consumer no longer binds the provider, before its entry is removed; an error
|
|
// keeps the entry, and it is heard again on the next pass. Retiring what was kept for it is the provider's
|
|
// (ADR 0230).
|
|
Gone func(ctx context.Context, e Entry) error
|
|
// Identity is the consumer's identity for an entry whose plan names none; ConsumerIdentity of the node and the
|
|
// module when not set.
|
|
Identity func(node, module string) string
|
|
// Every is how often a pass runs without a change watched; a minute when not set.
|
|
Every time.Duration
|
|
// Logf says what a pass did that differs from the last; the standard log when not set.
|
|
Logf func(format string, args ...any)
|
|
}
|
|
|
|
// Provider answers its consumers.
|
|
type Provider struct {
|
|
opts Options
|
|
|
|
mu sync.Mutex
|
|
assignments map[string][]byte
|
|
entries map[string][]byte
|
|
said map[string]string
|
|
poke chan struct{}
|
|
}
|
|
|
|
// New is a provider answering its consumers as opts say.
|
|
func New(opts Options) *Provider {
|
|
if opts.Identity == nil {
|
|
opts.Identity = ConsumerIdentity
|
|
}
|
|
if opts.Every <= 0 {
|
|
opts.Every = time.Minute
|
|
}
|
|
if opts.Logf == nil {
|
|
opts.Logf = func(f string, a ...any) { fmt.Printf("[provider] "+f+"\n", a...) }
|
|
}
|
|
return &Provider{opts: opts, assignments: map[string][]byte{}, entries: map[string][]byte{},
|
|
said: map[string]string{}, poke: make(chan struct{}, 1)}
|
|
}
|
|
|
|
// What a pass did for one bind.
|
|
const (
|
|
Answered = "answered"
|
|
Refused = "refused"
|
|
Waiting = "waiting"
|
|
Failed = "failed"
|
|
Removed = "removed"
|
|
)
|
|
|
|
// Outcome is what a pass did for one consumer's entry.
|
|
type Outcome struct {
|
|
Key string `json:"key"`
|
|
Status string `json:"status"`
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
// Run watches the binds and the provider's own entries, and passes once at start, on every change and every
|
|
// Every, until ctx ends.
|
|
func (p *Provider) Run(ctx context.Context) error {
|
|
if err := p.watch(ctx, BucketOf(p.opts.Module), p.entries); err != nil {
|
|
return fmt.Errorf("watching %s: %w", BucketOf(p.opts.Module), err)
|
|
}
|
|
if err := p.watch(ctx, AssignmentsBucket, p.assignments); err != nil {
|
|
return fmt.Errorf("watching %s: %w", AssignmentsBucket, err)
|
|
}
|
|
tick := time.NewTicker(p.opts.Every)
|
|
defer tick.Stop()
|
|
for {
|
|
p.mu.Lock()
|
|
assignments, entries := clone(p.assignments), clone(p.entries)
|
|
p.mu.Unlock()
|
|
if _, err := p.pass(ctx, assignments, entries); err != nil {
|
|
p.say("pass", err.Error())
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-p.poke:
|
|
case <-tick.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Provider) watch(ctx context.Context, bucket string, into map[string][]byte) error {
|
|
return p.opts.Bus.Watch(ctx, bucket, func(key string, value []byte) {
|
|
p.mu.Lock()
|
|
if value == nil {
|
|
delete(into, key)
|
|
} else {
|
|
into[key] = append([]byte(nil), value...)
|
|
}
|
|
p.mu.Unlock()
|
|
if bucket == AssignmentsBucket {
|
|
select {
|
|
case p.poke <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Pass reads the binds and the provider's entries from the bus and answers once: each bind of this provider gets
|
|
// its entry, and each entry of this provider no bind asks for is removed. An assignment that cannot be read stops
|
|
// the pass before anything is removed. Run passes by itself; Pass is for a provider that passes when it chooses.
|
|
func (p *Provider) Pass(ctx context.Context) ([]Outcome, error) {
|
|
watchCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
var mu sync.Mutex
|
|
read := func(bucket string) (map[string][]byte, error) {
|
|
got := map[string][]byte{}
|
|
err := p.opts.Bus.Watch(watchCtx, bucket, func(key string, value []byte) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if value == nil {
|
|
delete(got, key)
|
|
} else {
|
|
got[key] = append([]byte(nil), value...)
|
|
}
|
|
})
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return clone(got), err
|
|
}
|
|
entries, err := read(BucketOf(p.opts.Module))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading %s: %w", BucketOf(p.opts.Module), err)
|
|
}
|
|
assignments, err := read(AssignmentsBucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading %s: %w", AssignmentsBucket, err)
|
|
}
|
|
return p.pass(ctx, assignments, entries)
|
|
}
|
|
|
|
// planned is as much of an assignment as the helper reads: lenient, since the plan is the client's to act on.
|
|
type planned struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Provisioning *struct {
|
|
Provisions []struct {
|
|
Provision string `json:"provision"`
|
|
Local string `json:"local"`
|
|
Provider string `json:"provider"`
|
|
ProviderModule string `json:"provider-module"`
|
|
Contributes map[string]any `json:"contributes"`
|
|
At string `json:"at"`
|
|
From string `json:"from"`
|
|
As string `json:"as"`
|
|
Names map[string]string `json:"names"`
|
|
} `json:"provisions"`
|
|
} `json:"provisioning"`
|
|
}
|
|
|
|
func (p *Provider) pass(ctx context.Context, assignments, entries map[string][]byte) ([]Outcome, error) {
|
|
var binds []Entry
|
|
for _, key := range sortedKeys(assignments) {
|
|
var a planned
|
|
if err := json.Unmarshal(assignments[key], &a); err != nil || a.Node+"."+a.Module != key {
|
|
return nil, fmt.Errorf("the assignment %s is not the controller's, so who binds %s on %s is not known; "+
|
|
"nothing is answered or removed", key, p.opts.Module, p.opts.Node)
|
|
}
|
|
if a.Provisioning == nil {
|
|
continue
|
|
}
|
|
for _, b := range a.Provisioning.Provisions {
|
|
if b.Provider != p.opts.Node || b.ProviderModule != p.opts.Module {
|
|
continue
|
|
}
|
|
binds = append(binds, Entry{Provider: p.opts.Node, ProviderModule: p.opts.Module,
|
|
Provision: b.Provision, Consumer: a.Node, Module: a.Module, Local: b.Local,
|
|
Contributes: b.Contributes, At: b.At, From: b.From, As: b.As, Names: b.Names})
|
|
}
|
|
}
|
|
|
|
var out []Outcome
|
|
want := map[string]bool{}
|
|
for _, b := range binds {
|
|
key := b.Key()
|
|
want[key] = true
|
|
o := p.answer(ctx, b, entries[key])
|
|
out = append(out, o)
|
|
p.say(key, o.Status+" "+o.Why)
|
|
}
|
|
for _, key := range sortedKeys(entries) {
|
|
if want[key] {
|
|
continue
|
|
}
|
|
var e Entry
|
|
if err := json.Unmarshal(entries[key], &e); err != nil || e.Provider != p.opts.Node ||
|
|
e.ProviderModule != p.opts.Module {
|
|
continue // another machine's instance of this provider wrote it, or nothing of this helper's
|
|
}
|
|
if p.opts.Gone != nil {
|
|
if err := p.opts.Gone(ctx, e); err != nil {
|
|
o := Outcome{Key: key, Status: Failed, Why: "no longer bound, and the provider could not let it go: " +
|
|
err.Error()}
|
|
out = append(out, o)
|
|
p.say(key, o.Status+" "+o.Why)
|
|
continue
|
|
}
|
|
}
|
|
if err := p.opts.Bus.Delete(ctx, BucketOf(p.opts.Module), key); err != nil {
|
|
o := Outcome{Key: key, Status: Failed, Why: "no longer bound, and its entry could not be removed: " +
|
|
err.Error()}
|
|
out = append(out, o)
|
|
p.say(key, o.Status+" "+o.Why)
|
|
continue
|
|
}
|
|
out = append(out, Outcome{Key: key, Status: Removed})
|
|
p.say(key, Removed)
|
|
p.mu.Lock()
|
|
delete(p.said, key)
|
|
p.mu.Unlock()
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// answer makes one bind's entry and writes it when it differs from what the bucket holds.
|
|
func (p *Provider) answer(ctx context.Context, e Entry, have []byte) Outcome {
|
|
key := e.Key()
|
|
offer, offered := p.opts.Offers[e.Provision]
|
|
switch {
|
|
case !offered:
|
|
e.Refused = fmt.Sprintf("%s on %s is bound to %s on %s for %s, which %s does not serve", e.Module,
|
|
e.Consumer, e.ProviderModule, e.Provider, e.Provision, e.ProviderModule)
|
|
case e.Local != "" && derives(offer.Serves):
|
|
e.Refused = fmt.Sprintf("%s on %s keeps several holders of %s (this one is %q), and %s on %s derives %s for "+
|
|
"each consumer from its login: each holder has a login of its own and a consumer is told one value per "+
|
|
"requirement, so the two ends would name different things (novox/hq ADR 0202 §10, ADR 0349 clause 12)",
|
|
e.Module, e.Consumer, e.Provision, e.Local, e.ProviderModule, e.Provider,
|
|
strings.Join(derivedKeys(offer.Serves), ", "))
|
|
}
|
|
if e.Refused != "" {
|
|
return p.write(ctx, e, have, Outcome{Key: key, Status: Refused, Why: e.Refused})
|
|
}
|
|
|
|
// The plan's identity is the controller's, slug first; Identity only answers for an entry without one.
|
|
if e.As == "" {
|
|
e.As = p.opts.Identity(e.Consumer, e.Module)
|
|
}
|
|
values, err := ServedTo(offer.Serves, e.As)
|
|
if err != nil {
|
|
e.Refused = fmt.Sprintf("%s on %s serving %s to %s on %s: %v", e.ProviderModule, e.Provider, e.Provision,
|
|
e.Module, e.Consumer, err)
|
|
e.As = ""
|
|
return p.write(ctx, e, have, Outcome{Key: key, Status: Refused, Why: e.Refused})
|
|
}
|
|
if e.At != "" || e.From != "" {
|
|
if values == nil {
|
|
values = map[string]any{}
|
|
}
|
|
if e.At != "" {
|
|
values["at"] = e.At
|
|
}
|
|
if e.From != "" {
|
|
values["from"] = e.From
|
|
}
|
|
}
|
|
if len(e.Names) > 0 {
|
|
if values == nil {
|
|
values = map[string]any{}
|
|
}
|
|
for key, name := range e.Names {
|
|
values[key] = name
|
|
}
|
|
}
|
|
e.Values = values
|
|
var credential []byte
|
|
switch offer.Credential {
|
|
case Shared:
|
|
e.User = offer.User
|
|
e.Credential = SharedName(e.Provider, e.Provision)
|
|
case PerConsumer:
|
|
e.User = e.As
|
|
e.Credential = PairName(e.Provider, e.Provision, e.Consumer, e.Module, e.Local)
|
|
}
|
|
if e.Credential != "" {
|
|
credential, err = p.opts.Vault.Value(ctx, e.Credential)
|
|
if err != nil {
|
|
return Outcome{Key: key, Status: Waiting, Why: fmt.Sprintf("the vault's %s: %v", e.Credential, err)}
|
|
}
|
|
}
|
|
if p.opts.Make != nil {
|
|
if err := p.opts.Make(ctx, Consumer{Entry: e, Credential: credential}); err != nil {
|
|
why := err.Error()
|
|
if len(credential) > 0 {
|
|
why = strings.ReplaceAll(why, string(credential), "<credential>")
|
|
}
|
|
return Outcome{Key: key, Status: Failed, Why: "the provider could not make it: " + why}
|
|
}
|
|
}
|
|
return p.write(ctx, e, have, Outcome{Key: key, Status: Answered})
|
|
}
|
|
|
|
func (p *Provider) write(ctx context.Context, e Entry, have []byte, o Outcome) Outcome {
|
|
body, err := json.Marshal(e)
|
|
if err != nil {
|
|
return Outcome{Key: o.Key, Status: Failed, Why: err.Error()}
|
|
}
|
|
if sameJSON(have, body) {
|
|
return o
|
|
}
|
|
if err := p.opts.Bus.Put(ctx, BucketOf(p.opts.Module), o.Key, body); err != nil {
|
|
return Outcome{Key: o.Key, Status: Failed, Why: "its entry could not be written: " + err.Error()}
|
|
}
|
|
return o
|
|
}
|
|
|
|
// say logs what a key's pass said when it differs from the last.
|
|
func (p *Provider) say(key, what string) {
|
|
p.mu.Lock()
|
|
same := p.said[key] == what
|
|
p.said[key] = what
|
|
p.mu.Unlock()
|
|
if !same {
|
|
p.opts.Logf("%s: %s", key, strings.TrimSpace(what))
|
|
}
|
|
}
|
|
|
|
// SharedName is the vault's one value per provider and provision (ADR 0158, issue 677).
|
|
func SharedName(provider, provision string) string { return "provision/" + provider + "/" + provision }
|
|
|
|
// PairName is the vault's value of one consumer's own login (ADR 0048, ADR 0158 §11, issue 677's note 12).
|
|
func PairName(provider, provision, node, module, local string) string {
|
|
n := "pair/" + provider + "/" + provision + "/" + node + "/" + module
|
|
if local != "" {
|
|
n += "/" + local
|
|
}
|
|
return n
|
|
}
|
|
|
|
// identityUnusable is every character a consumer's identity does not hold (mesh-controller internal/catalogue
|
|
// identity.go, the one derivation).
|
|
var identityUnusable = regexp.MustCompile(`[^a-z0-9_]+`)
|
|
|
|
// ConsumerIdentity is what one module on one machine is called wherever it authenticates: `mesh_<node>_<module>`,
|
|
// lower case, anything but letters, digits and `_` written `_` (mesh-controller ConsumerIdentity). module is the
|
|
// identity source: the module's slug when it declares one, otherwise its name (ADR 0049).
|
|
func ConsumerIdentity(node, module string) string {
|
|
clean := func(s string) string {
|
|
return strings.Trim(identityUnusable.ReplaceAllString(strings.ToLower(s), "_"), "_")
|
|
}
|
|
return "mesh_" + clean(node) + "_" + clean(module)
|
|
}
|
|
|
|
// consumerFact is `${consumer:<fact>}` or `${consumer:<fact>:<alphabet>}`.
|
|
var consumerFact = regexp.MustCompile(`\$\{consumer:([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\}`)
|
|
|
|
// ServedTo fills an offer's served values for one consumer whose identity is as (ADR 0202 §1): `${consumer:as}` as
|
|
// it is, `${consumer:as:dns}` with `-` for `_`. Only strings carry placeholders; any other fact or alphabet is
|
|
// refused, never left standing.
|
|
func ServedTo(serves map[string]any, as string) (map[string]any, error) {
|
|
if serves == nil {
|
|
return nil, nil
|
|
}
|
|
out := make(map[string]any, len(serves))
|
|
for _, key := range sortedKeys(serves) {
|
|
text, ok := serves[key].(string)
|
|
if !ok || !strings.Contains(text, "${consumer:") {
|
|
out[key] = serves[key]
|
|
continue
|
|
}
|
|
var failed error
|
|
out[key] = consumerFact.ReplaceAllStringFunc(text, func(match string) string {
|
|
parts := consumerFact.FindStringSubmatch(match)
|
|
switch {
|
|
case parts[1] == "as" && parts[2] == "":
|
|
return as
|
|
case parts[1] == "as" && parts[2] == "dns":
|
|
return strings.ReplaceAll(as, "_", "-")
|
|
}
|
|
if failed == nil {
|
|
failed = fmt.Errorf("the value served as %q says %s, and the mesh states only ${consumer:as} and "+
|
|
"${consumer:as:dns}", key, match)
|
|
}
|
|
return match
|
|
})
|
|
if failed != nil {
|
|
return nil, failed
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func derives(serves map[string]any) bool { return len(derivedKeys(serves)) > 0 }
|
|
|
|
func derivedKeys(serves map[string]any) []string {
|
|
var out []string
|
|
for _, k := range sortedKeys(serves) {
|
|
if text, ok := serves[k].(string); ok && strings.Contains(text, "${consumer:") {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func clone(m map[string][]byte) map[string][]byte {
|
|
out := make(map[string][]byte, len(m))
|
|
for k, v := range m {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sameJSON(a, b []byte) bool {
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
var x, y any
|
|
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
|
|
return false
|
|
}
|
|
ja, _ := json.Marshal(x)
|
|
jb, _ := json.Marshal(y)
|
|
return string(ja) == string(jb)
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// StateBus is the Bus of a provider that runs as a module bundle over the runtime's stdio: the provider's own bucket,
|
|
// BucketOf(module), is the module's state `bound`, and AssignmentsBucket is the state it reads,
|
|
// `mesh-controller.assignments` (both declared in the module's manifest). Any other bucket is refused.
|
|
//
|
|
// A stdio state watch lasts as long as the process and cannot be ended (stdio.KeptState.Watch), so Watch honours the
|
|
// Bus contract by silencing instead: once ctx has ended the handler is never called again, though the runtime keeps
|
|
// sending changes that are dropped. A Pass, which watches twice per call, is therefore correct, only the dropped
|
|
// changes of its two spent watches remain.
|
|
func StateBus(module string) Bus { return stateBus{module: module} }
|
|
|
|
type stateBus struct{ module string }
|
|
|
|
func (b stateBus) state(bucket string) (stdio.KeptState, error) {
|
|
switch bucket {
|
|
case BucketOf(b.module):
|
|
return stdio.State("bound"), nil
|
|
case AssignmentsBucket:
|
|
return stdio.State("mesh-controller.assignments"), nil
|
|
}
|
|
return stdio.KeptState{}, fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket)
|
|
}
|
|
|
|
func (b stateBus) Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error {
|
|
s, err := b.state(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.Watch("", func(ch stdio.StateChange) error {
|
|
if ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
if ch.Op == "delete" {
|
|
handle(ch.Key, nil)
|
|
} else {
|
|
handle(ch.Key, ch.Value)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (b stateBus) Put(_ context.Context, bucket, key string, value []byte) error {
|
|
s, err := b.state(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.Put(key, json.RawMessage(value))
|
|
return err
|
|
}
|
|
|
|
func (b stateBus) Delete(_ context.Context, bucket, key string) error {
|
|
s, err := b.state(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.Delete(key)
|
|
}
|