Lay out a provision as a key of mesh-provisioner holding a reference and never a value, and a plan as a key of its own, so the holder, the vault, the clients and the providers read one record (issue 1954 L6-P2, ADR 0570 clauses 1, 2)

This commit is contained in:
2026-10-29 22:32:15 +01:00
parent 233b6be5dd
commit 4a967933b3
3 changed files with 299 additions and 4 deletions
+1 -4
View File
@@ -53,10 +53,7 @@ func TestWhereAModulesUpgradePolicyComesFrom(t *testing.T) {
t.Errorf("%+v was accepted", p)
}
}
}
// A policy other than roll says why, and an unknown one is refused.
func TestAnUpgradePolicySaysWhy(t *testing.T) {
// A policy other than roll says why, and an unknown one is refused.
for _, u := range []UpgradePolicy{{Policy: PolicyRecord}, {Policy: PolicyTogether}, {Policy: "sometimes", Why: "x"}} {
if problems := (Manifest{Module: "m", Upgrade: &u}).upgradeProblems(); len(problems) == 0 {
t.Errorf("%+v was accepted", u)
+220
View File
@@ -0,0 +1,220 @@
// 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"
"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
}
// 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).
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
}
+78
View File
@@ -0,0 +1,78 @@
package provisions
import (
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// ADR 0570 clauses 1, 2 and 5 (issue 1954 L6-P2): a provision's key is its consumer's node and module, the provision and
// its local name, the provider's entry's key; its value round-trips, names its state and why, and refuses any field the
// layout does not name, so no value rides in it; a plan's key is its node and module.
func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) {
at := time.Date(2026, 10, 29, 22, 0, 0, 0, time.UTC)
db := Provision{Provision: "postgres-database", Consumer: End{Module: "keycloak", Node: "novox"},
Provider: End{Module: "postgres", Node: "novox"}, DependsOn: []string{"novox.keycloak.secret.db"}, State: Asked,
Reference: &Reference{ID: "provision/postgres/postgres-database", Generation: 2}, At: at}
for _, c := range []struct {
name string
edit func(*Provision)
key string
refused string
}{
{name: "asked, depending on the vault's", key: "novox.keycloak.postgres-database"},
{name: "under a local name", edit: func(p *Provision) { p.Local = "replica" }, key: "novox.keycloak.postgres-database.replica"},
{name: "retiring with the operator's warrant", edit: func(p *Provision) {
p.State, p.Why, p.Ask = Retiring, "keycloak left novox", "a-1"
p.Warrant = &Warrant{Ask: "a-1", Option: "yes", By: "the operator", At: at}
}, key: "novox.keycloak.postgres-database"},
{name: "an unknown state", edit: func(p *Provision) { p.State = "done" }, refused: "one of"},
{name: "failed without why", edit: func(p *Provision) { p.State = Failed }, refused: "says why"},
{name: "a warrant on a provided one", edit: func(p *Provision) { p.State, p.Warrant = Provided, &Warrant{Ask: "a"} }, refused: "only a retiring"},
{name: "no provider", edit: func(p *Provision) { p.Provider = End{} }, refused: "no provider"},
{name: "a dotted consumer", edit: func(p *Provision) { p.Consumer.Module = "a.b" }, refused: "cannot be part"},
{name: "a dependency that is no key", edit: func(p *Provision) { p.DependsOn = []string{"x"} }, refused: "is not"},
} {
p := db
if c.edit != nil {
c.edit(&p)
}
raw, err := Encode(p)
if c.refused != "" {
if err == nil || !strings.Contains(err.Error(), c.refused) {
t.Errorf("%s: %v, want refused saying %q", c.name, err, c.refused)
}
continue
}
back, err := Decode(raw)
key, kerr := back.Key()
if err != nil || kerr != nil || key != c.key || back.V != Version || back.Reference.Generation != 2 {
t.Errorf("%s: read back as %+v under %q, %v %v", c.name, back, key, err, kerr)
}
if node, consumer, provision, local, err := ParseKey(key); err != nil || node != "novox" || consumer != "keycloak" ||
provision != "postgres-database" || local != p.Local {
t.Errorf("%s: %s parses as %s %s %s %s, %v", c.name, key, node, consumer, provision, local, err)
}
}
// A value never rides in a provision: a field the layout does not name is refused.
if _, err := Decode([]byte(`{"v":1,"provision":"p","consumer":{"module":"c","node":"n"},"provider":{"module":"p","node":"n"},` +
`"state":"provided","at":"2026-10-29T22:00:00Z","password":"hunter2"}`)); err == nil {
t.Error("a provision carrying a password was read")
}
plan := Plan{Node: "novox", Module: "restic", Plan: catalogue.Plan{}}
raw, err := EncodePlan(plan)
if err != nil {
t.Fatal(err)
}
if back, err := DecodePlan(raw); err != nil || back.Node != "novox" || back.Module != "restic" {
t.Errorf("a plan read back as %+v, %v", back, err)
}
if k, err := PlanKey("novox", "restic"); k != "novox.restic" || err != nil {
t.Errorf("a plan's key is %q, %v", k, err)
}
if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" {
t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead)
}
}