Files
mesh-sdk/go/provisions/provisions.go
T
jschoubben c94f30d398 Bind a consumer from the plan entry its key carries, not only from the plans
The seat keys a consumer's provisions from its wanted and newest builds but
keeps the plan of its wanted build alone, and the provider helper bound only
from plans: a requirement the newest build adds stayed asked with no word,
and the want waiting for it never moved (mcp-endpoint on ace and shanks).
A provision now carries the plan entry it was composed from; the helper
binds from it where no plan names the key, the plan winning where both do,
and says each key it can bind from neither (issue 1126 review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:17:20 +01:00

311 lines
12 KiB
Go

// Package provisions is the contract of the seat mesh-provisioner's records (novox/hq ADR 0570 clauses 1, 2 and 5; issue
// 1954): every provision one key of the state `provisions`, holding a reference and never a value, and every handed
// module's provisioning plan one key of the state `plans`. The module mesh-provisioner alone writes both; the vault,
// each node's provisioner client and every provider read them through `reads`.
package provisions
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The seat, its holder and its two states, and the names a reader declares in its `reads`.
const (
Seat = "mesh-provisioner"
Module = "mesh-provisioner"
ProvisionsState = "provisions"
PlansState = "plans"
ProvisionsRead = Module + "." + ProvisionsState
PlansRead = Module + "." + PlansState
// ProvisionsBucket and PlansBucket are the states' buckets.
ProvisionsBucket = Module + "_" + ProvisionsState
PlansBucket = Module + "_" + PlansState
)
// The states of a provision (clause 1).
const (
// Asked is a provision its consumer needs that its provider has not answered yet, or whose dependencies are not
// all provided (clause 3).
Asked = "asked"
// Provided is a provision the provider said it made: set only from the provider's outcome (clause 3).
Provided = "provided"
// Waits is a provision only a person can give, naming what is awaited (clause 7).
Waits = "waits"
// Failed is a provision the provider could not make, with why.
Failed = "failed"
// Retiring is a provision no want needs, asked of the operator before its credential goes (clause 5).
Retiring = "retiring"
)
// States is every state a provision may be in.
var States = []string{Asked, Provided, Waits, Failed, Retiring}
// Version is the version of the records' layout.
const Version = 1
// token is one part of a key: what a bucket's key takes, no dot.
var token = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// Key is a provision's key: `<node>.<consumer>.<provision>`, and `.<local>` where the consumer keeps several values of
// one provision (ADR 0094). The same key the provider's own entry for that consumer has (provider.Entry.Key).
func Key(node, consumer, provision, local string) (string, error) {
parts := []string{node, consumer, provision}
if local != "" {
parts = append(parts, local)
}
for _, p := range parts {
if !token.MatchString(p) {
return "", fmt.Errorf("provisions: %q cannot be part of a provision's key", p)
}
}
return strings.Join(parts, "."), nil
}
// ParseKey is the inverse of Key.
func ParseKey(key string) (node, consumer, provision, local string, err error) {
parts := strings.Split(key, ".")
if len(parts) < 3 || len(parts) > 4 {
return "", "", "", "", fmt.Errorf("provisions: %q is not <node>.<consumer>.<provision>[.<local>]", key)
}
for _, p := range parts {
if !token.MatchString(p) {
return "", "", "", "", fmt.Errorf("provisions: %q is not <node>.<consumer>.<provision>[.<local>]", key)
}
}
if len(parts) == 4 {
local = parts[3]
}
return parts[0], parts[1], parts[2], local, nil
}
// PlanKey is a plan's key in `plans`: `<node>.<module>` (clause 2).
func PlanKey(node, module string) (string, error) {
if !token.MatchString(node) || !token.MatchString(module) {
return "", fmt.Errorf("provisions: %s.%s is not a plan's key, <node>.<module>", node, module)
}
return node + "." + module, nil
}
// The forms of a provision's credential, as a plan's entry names them (catalogue.PlanProvision.CredentialForm): the
// catalogue's one list (catalogue.CredentialForms, issue 774), under the names this package has used.
const (
FormPair = catalogue.CredentialFormPair
FormShared = catalogue.CredentialFormShared
FormIssued = catalogue.CredentialFormIssued
FormGiven = catalogue.CredentialFormGiven
// FormNone is an offer whose consumers present no credential (catalogue.CredentialFormNone): the vault mints nothing,
// and the provision depends on no credential.
FormNone = catalogue.CredentialFormNone
)
// VaultName is the vault's name of a provision's credential in form, as its verb `own` names it and the provider asks
// it: `pair/<provider node>/<provision>/<consumer node>/<module>[/<local>]` for a login of the consumer's own (ADR 0158
// §11), `provision/<provider node>/<provision>` for the provider's one shared value, and `provision/<provider
// node>/<provision>/<consumer node>/<module>` for a holder's own issued or given value. Empty for a form with no
// credential.
func VaultName(form string, p Provision) string {
switch form {
case FormPair:
n := "pair/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module
if p.Local != "" {
n += "/" + p.Local
}
return n
case FormShared:
return "provision/" + p.Provider.Node + "/" + p.Provision
case FormIssued, FormGiven:
return "provision/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module
}
return ""
}
// VaultModule is the provider of every provision's credential (novox/hq ADR 0570 clauses 1, 6).
const VaultModule = "mesh-vault"
// CredentialKey is the key of a provision's credential, itself a provision whose provider is the vault (clause 6; the
// key form the lead settled on 2026-10-29): `<consumer node>.<consumer>.secret-<provision>[.<local>]` for a value of the
// consumer's own (pair, issued, given), and `<provider node>.<provider>.secret-<provision>` for the provider's one
// shared value. False for a form with no vault value, and for a provision the vault itself provides.
func CredentialKey(form string, consumer, provider End, provision, local string) (string, bool) {
if provider.Module == VaultModule {
return "", false // the vault's own offer is the value: it has no credential beside it (the lead, 2026-10-29)
}
name := catalogue.CredentialProvisionPrefix + provision
switch form {
case FormPair, FormIssued, FormGiven:
k, err := Key(consumer.Node, consumer.Module, name, local)
return k, err == nil
case FormShared:
k, err := Key(provider.Node, provider.Module, name, "")
return k, err == nil
}
return "", false
}
// DependsOn is what a provision waits for before its provider makes it (clause 3): its credential's key, where its form
// has a vault value, and the key of every provision its provider module itself requires on its own node, read from
// the provider's plan there (nil when the provider is not handed and has none). Sorted, none twice.
func DependsOn(consumer End, entry catalogue.PlanProvision, providerPlan *catalogue.Plan) []string {
seen := map[string]bool{}
var out []string
add := func(k string) {
if k != "" && !seen[k] {
seen[k] = true
out = append(out, k)
}
}
provider := End{Module: entry.ProviderModule, Node: entry.Provider}
if k, ok := CredentialKey(entry.CredentialForm, consumer, provider, entry.Provision, entry.Local); ok {
add(k)
}
if providerPlan != nil {
for _, own := range providerPlan.Provisions {
if k, err := Key(provider.Node, provider.Module, own.Provision, own.Local); err == nil {
add(k)
}
}
}
sort.Strings(out)
return out
}
// End is one end of a provision: a module on a node.
type End struct {
Module string `json:"module"`
Node string `json:"node"`
}
// Reference is a value the vault keeps, by its id and the generation of the value: never the value, sealed or not
// (ADR 0349 §7). The holder writes the generation the vault says in its own entry (clause 6). ID is the vault's name of
// the credential exactly as its verb `own` names it (VaultName): the consuming node's engine checks both the name and
// the generation of what it is handed.
type Reference struct {
ID string `json:"id"`
Generation int `json:"generation"`
}
// Warrant is the operator's answer to a retiring provision's ask, as the holder writes it into the key (clause 5).
type Warrant struct {
Ask string `json:"ask"`
Option string `json:"option"`
By string `json:"by"`
At time.Time `json:"at"`
}
// Provision is one key of `provisions`.
type Provision struct {
V int `json:"v"`
Provision string `json:"provision"`
Local string `json:"local,omitempty"`
Consumer End `json:"consumer"`
Provider End `json:"provider"`
// DependsOn are the keys of the provisions this one waits for (clause 3): a database user whose password is the
// vault's.
DependsOn []string `json:"depends-on,omitempty"`
State string `json:"state"`
// Why is the reason for waits, failed and retiring, and for asked when a dependency holds it.
Why string `json:"why,omitempty"`
Reference *Reference `json:"reference,omitempty"`
// Ask is a retiring provision's ask of the operator; Warrant the answer, once given.
Ask string `json:"ask,omitempty"`
Warrant *Warrant `json:"warrant,omitempty"`
// Plan is the plan entry the key was composed from, at the consumer's wanted build or its newest (issue 1126): the
// provider binds the consumer from it, so a requirement the newest build adds is provided before the want that
// waits for it moves, though the plan the seat keeps at the wanted build names it not yet.
Plan *catalogue.PlanProvision `json:"plan,omitempty"`
At time.Time `json:"at"`
}
// Key is the provision's key.
func (p Provision) Key() (string, error) {
return Key(p.Consumer.Node, p.Consumer.Module, p.Provision, p.Local)
}
func (p Provision) check() error {
if _, err := p.Key(); err != nil {
return err
}
if p.Provider.Module == "" || p.Provider.Node == "" {
return fmt.Errorf("provisions: %s names no provider", p.Provision)
}
known := false
for _, s := range States {
known = known || s == p.State
}
switch {
case !known:
return fmt.Errorf("provisions: a provision's state is one of %s, not %q", strings.Join(States, ", "), p.State)
case (p.State == Waits || p.State == Failed || p.State == Retiring) && strings.TrimSpace(p.Why) == "":
return fmt.Errorf("provisions: a %s provision says why", p.State)
case p.Warrant != nil && p.State != Retiring:
return fmt.Errorf("provisions: only a retiring provision carries the operator's warrant")
}
for _, d := range p.DependsOn {
if _, _, _, _, err := ParseKey(d); err != nil {
return err
}
}
return nil
}
// Encode is p as JSON at Version; it refuses what a holder may not write.
func Encode(p Provision) ([]byte, error) {
p.V = Version
if err := p.check(); err != nil {
return nil, err
}
return json.Marshal(p)
}
// Decode reads a key's value strictly: a field the layout does not name is refused, so no value, sealed or not, rides in
// a provision (clause 1, ADR 0201 §13).
func Decode(data []byte) (Provision, error) {
var p Provision
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&p); err != nil {
return Provision{}, fmt.Errorf("provisions: a provision is not readable: %w", err)
}
if p.V != Version {
return Provision{}, fmt.Errorf("provisions: a provision of version %d, and this reads %d", p.V, Version)
}
return p, p.check()
}
// Plan is one key of `plans`: a module's provisioning plan on a node, never a value (clause 2).
type Plan struct {
V int `json:"v"`
Node string `json:"node"`
Module string `json:"module"`
Plan catalogue.Plan `json:"plan"`
}
// EncodePlan is p as JSON at Version.
func EncodePlan(p Plan) ([]byte, error) {
p.V = Version
if _, err := PlanKey(p.Node, p.Module); err != nil {
return nil, err
}
return json.Marshal(p)
}
// DecodePlan reads a plan's key.
func DecodePlan(data []byte) (Plan, error) {
var p Plan
if err := json.Unmarshal(data, &p); err != nil {
return Plan{}, fmt.Errorf("provisions: a plan is not readable: %w", err)
}
if p.V != Version {
return Plan{}, fmt.Errorf("provisions: a plan of version %d, and this reads %d", p.V, Version)
}
_, err := PlanKey(p.Node, p.Module)
return p, err
}