Provision and provider users take the provisioner's client; mesh-vault writes its findings through the conditions client (K-4..K-20, K-21 part; ADR 0634 §4; issue 1966)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -78,14 +78,14 @@ const ProvisionerPrefix = "provisioner."
|
||||
// seat mesh-provisioner's state `plans`, `<node>.<module>` (novox/hq ADR 0570 clause 2), no longer the controller's
|
||||
// assignments. A plan is read as the assignment's value carried it (asAssignment), so the rules that read a plan are
|
||||
// unchanged.
|
||||
const AssignmentsRead = provisions.PlansRead
|
||||
const AssignmentsRead = meshprovisionerclient.PlansRead
|
||||
|
||||
// asAssignment is a plan's key in the form the vault's rules read: `{node, module, provisioning: plan}`, nil for none.
|
||||
func asAssignment(key string, raw []byte, err error) ([]byte, error) {
|
||||
if err != nil || raw == nil {
|
||||
return raw, err
|
||||
}
|
||||
p, err := provisions.DecodePlan(raw)
|
||||
p, err := meshprovisionerclient.DecodePlan(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the plan %s: %w", key, err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -79,7 +80,7 @@ func build() *Env {
|
||||
Delete: func(key string) error { return stdio.State(BoundState).Delete(key) },
|
||||
Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() },
|
||||
Provision: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(provisions.ProvisionsRead).Get(key)
|
||||
e, err := stdio.State(meshprovisionerclient.ProvisionsRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,11 +123,8 @@ func watchLeavers(env *Env) bool {
|
||||
if node, ok := leaverKey(key); ok {
|
||||
env.Vault.NodeLeft(node)
|
||||
}
|
||||
}, env.Vault.PlacementChanged, func(key string, v json.RawMessage) error {
|
||||
_, err := stdio.State(FindingsState).Put(key, v)
|
||||
return err
|
||||
},
|
||||
func(key string) error { return stdio.State(FindingsState).Delete(key) })
|
||||
}, env.Vault.PlacementChanged,
|
||||
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
|
||||
beginPlacementView(placementView)
|
||||
}
|
||||
return true
|
||||
@@ -222,7 +220,7 @@ func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
err := stdio.State(provisions.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
|
||||
err := stdio.State(meshprovisionerclient.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
|
||||
if !c.Current && credentialAsked(c.Value) {
|
||||
select {
|
||||
case kick <- struct{}{}:
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
// The vault's outcome for each credential it provides (novox/hq ADR 0570 clauses 3, 6, 7): an entry in its own state
|
||||
// `bound`, one per credential provision `<node>.<consumer>.secret-<provision>[.<local>]`, which the holder of
|
||||
// mesh-provisioner reads to set that key's state. The entry names the value by the vault's own name for it
|
||||
// (provisions.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
|
||||
// (meshprovisionerclient.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
|
||||
// provided; one only the provider's software issues, or only a person gives, waits for them; one the vault has not
|
||||
// made yet has no entry, and the key stays asked. An offer whose form is none has no credential and no entry. An entry
|
||||
// is let go only once its key is retiring with the operator's warrant, or gone.
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
)
|
||||
|
||||
// BoundState is the vault's state of outcome entries.
|
||||
const BoundState = "bound"
|
||||
|
||||
// Bound is the vault's own state `bound`, and what it reads of the seat's provisions.
|
||||
// Bound is the vault's own state `bound`, and what it reads of the seat's meshprovisionerclient.
|
||||
type Bound struct {
|
||||
Get func(key string) ([]byte, error)
|
||||
Put func(key string, value []byte) error
|
||||
@@ -96,8 +96,8 @@ func (v *Vault) letGo(key string) bool {
|
||||
if raw == nil {
|
||||
return true
|
||||
}
|
||||
p, err := provisions.Decode(raw)
|
||||
return err == nil && p.State == provisions.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
|
||||
p, err := meshprovisionerclient.Decode(raw)
|
||||
return err == nil && p.State == meshprovisionerclient.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
|
||||
}
|
||||
|
||||
// outcomes is every credential's outcome the plans call for, by its key.
|
||||
@@ -119,42 +119,42 @@ func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
|
||||
continue
|
||||
}
|
||||
consumer := provisions.End{Module: a.Module, Node: a.Node}
|
||||
consumer := meshprovisionerclient.End{Module: a.Module, Node: a.Node}
|
||||
for _, p := range a.Provisioning.Provisions {
|
||||
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
|
||||
if p.ProviderModule == provisions.VaultModule && p.Provision == "secret" {
|
||||
provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
|
||||
if p.ProviderModule == meshprovisionerclient.VaultModule && p.Provision == "secret" {
|
||||
// The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
|
||||
// soon as it runs.
|
||||
key, err := provisions.Key(a.Node, a.Module, p.Provision, p.Local)
|
||||
key, err := meshprovisionerclient.Key(a.Node, a.Module, p.Provision, p.Local)
|
||||
if err == nil {
|
||||
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: p.Provision,
|
||||
Consumer: a.Node, Module: a.Module, Local: p.Local, State: provisions.Provided,
|
||||
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: p.Provision,
|
||||
Consumer: a.Node, Module: a.Module, Local: p.Local, State: meshprovisionerclient.Provided,
|
||||
Why: "the vault hands it to the consumer's client on request"}
|
||||
}
|
||||
continue
|
||||
}
|
||||
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
|
||||
ck, ok := meshprovisionerclient.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
|
||||
if !ok {
|
||||
continue // form none: no credential, nothing minted
|
||||
}
|
||||
parent := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
|
||||
name := provisions.VaultName(p.CredentialForm, parent)
|
||||
cn, cm, cp, cl, err := provisions.ParseKey(ck)
|
||||
parent := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
|
||||
name := meshprovisionerclient.VaultName(p.CredentialForm, parent)
|
||||
cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
|
||||
if err != nil || name == "" {
|
||||
continue
|
||||
}
|
||||
o := BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: cp, Consumer: cn,
|
||||
o := BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: cp, Consumer: cn,
|
||||
Module: cm, Local: cl, VaultName: name}
|
||||
rec, held, err := v.Store.Read(name)
|
||||
switch {
|
||||
case err != nil:
|
||||
o.State, o.Why = provisions.Failed, "the vault cannot read "+name+": "+err.Error()
|
||||
o.State, o.Why = meshprovisionerclient.Failed, "the vault cannot read "+name+": "+err.Error()
|
||||
case held:
|
||||
o.State, o.Generation = provisions.Provided, rec.Generation
|
||||
o.State, o.Generation = meshprovisionerclient.Provided, rec.Generation
|
||||
case p.CredentialForm == FormIssued:
|
||||
o.State, o.Why = provisions.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
|
||||
o.State, o.Why = meshprovisionerclient.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
|
||||
case p.CredentialForm == FormGiven:
|
||||
o.State, o.Why = provisions.Waits, "a person gives it (ADR 0337): `accept` brings it in"
|
||||
o.State, o.Why = meshprovisionerclient.Waits, "a person gives it (ADR 0337): `accept` brings it in"
|
||||
default:
|
||||
continue // not made yet: the key stays asked until the pair pass makes it
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
@@ -865,7 +865,7 @@ func (e *Env) credentialsState() []PairOutcome {
|
||||
// credentialAsked is whether a key of the seat mesh-provisioner's provisions asks the vault for a credential (ADR 0570
|
||||
// §1, §6): a provision `secret-<provision>` whose provider is the vault, in state asked.
|
||||
func credentialAsked(raw []byte) bool {
|
||||
p, err := provisions.Decode(raw)
|
||||
return err == nil && p.State == provisions.Asked && p.Provider.Module == provisions.VaultModule &&
|
||||
p, err := meshprovisionerclient.Decode(raw)
|
||||
return err == nil && p.State == meshprovisionerclient.Asked && p.Provider.Module == meshprovisionerclient.VaultModule &&
|
||||
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ var placementView *views.Follower
|
||||
// newPlacementView makes the follower: ask calls the tool, gone is told of a deleted key, changed of every change after
|
||||
// it is held (a first view's keys too).
|
||||
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(),
|
||||
put func(key string, value json.RawMessage) error, drop func(key string) error) *views.Follower {
|
||||
findings views.FindingWriter) *views.Follower {
|
||||
return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say,
|
||||
Ask: func(_ context.Context, after string) (views.Answer, error) {
|
||||
body := map[string]any{"values": true}
|
||||
@@ -55,16 +55,13 @@ func newPlacementView(ask func(key string, body any) (json.RawMessage, error), g
|
||||
Raise: func(s views.Status) {
|
||||
say("the vault has no first view of the assigner's placement: %s has not answered since %s (%s); it keeps asking", s.Tool,
|
||||
s.Asked.Format(time.RFC3339), s.Error)
|
||||
if k, v, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
|
||||
if err := put(k, v); err != nil {
|
||||
say("the finding %s is not written: %v", k, err)
|
||||
}
|
||||
f := views.UnansweredFinding(Seat, s, time.Now())
|
||||
if err := findings.Put(f); err != nil {
|
||||
say("the finding %s is not written: %v", f.ID, err)
|
||||
}
|
||||
},
|
||||
Clear: func(s views.Status) {
|
||||
if k, _, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
|
||||
_ = drop(k)
|
||||
}
|
||||
_ = findings.Delete(views.UnansweredFinding(Seat, s, time.Now()))
|
||||
},
|
||||
Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) {
|
||||
if deleted {
|
||||
|
||||
@@ -28,9 +28,7 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
|
||||
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
|
||||
return raw, nil
|
||||
}
|
||||
put := func(k string, v json.RawMessage) error { mu.Lock(); held[k] = v; mu.Unlock(); return nil }
|
||||
drop := func(k string) error { mu.Lock(); delete(held, k); mu.Unlock(); return nil }
|
||||
f := newPlacementView(ask, func(string) {}, nil, put, drop)
|
||||
f := newPlacementView(ask, func(string) {}, nil, heldFindings{&mu, held})
|
||||
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
|
||||
reader := viewPlacement(func() *views.Follower { return f })
|
||||
if _, err := reader.Keys(); err == nil {
|
||||
@@ -57,3 +55,22 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
|
||||
t.Errorf("keys %v, %v", keys, err)
|
||||
}
|
||||
}
|
||||
|
||||
// heldFindings keeps the findings written, by the finding's kind and subject.
|
||||
type heldFindings struct {
|
||||
mu *sync.Mutex
|
||||
held map[string]json.RawMessage
|
||||
}
|
||||
|
||||
func (h heldFindings) Put(f views.Finding) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.held[f.ID+"."+f.Kind] = json.RawMessage(f.Summary)
|
||||
return nil
|
||||
}
|
||||
func (h heldFindings) Delete(f views.Finding) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.held, f.ID+"."+f.Kind)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -196,16 +196,15 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
|
||||
// And its findings (ADR 0615 §6): the finding of a first view unanswered, written and deleted, never a secret.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Put(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Delete(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)}", "", 1))
|
||||
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and
|
||||
// one read of the seat's provisions, which hold none.
|
||||
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
|
||||
}
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.ProvisionsRead).Get(", "", 1))
|
||||
// And one watch of them, for a credential asked of the vault (issue 1126).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.ProvisionsRead).Watch(", "", 1))
|
||||
}
|
||||
if m := bucket.Find(code); m != nil {
|
||||
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-vault
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-20261104174344-ae5266e77639
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
|
||||
Reference in New Issue
Block a user