306 lines
11 KiB
Go
306 lines
11 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).
|
|
const (
|
|
FormPair = "pair"
|
|
FormShared = "shared"
|
|
FormIssued = "issued"
|
|
FormGiven = "given"
|
|
// FormNone is an offer whose consumers present no credential (catalogue.CredentialFormNone): the vault mints nothing,
|
|
// and the provision depends on no credential.
|
|
FormNone = "none"
|
|
)
|
|
|
|
// 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"`
|
|
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
|
|
}
|