L3: mesh-vault mints, retires and signs; nats makes its certificate; messenger asks each engine (issue 774, ADR 0561), rebased onto L2 #556
@@ -58,32 +58,6 @@ type adoptionKept struct {
|
||||
failed string // why the last look found nothing to adopt, or ""
|
||||
}
|
||||
|
||||
// busNode is the node the assignments name as holding the bus module: exactly one.
|
||||
func (v *Vault) busNode() (string, error) {
|
||||
if v.Assignments.Keys == nil {
|
||||
return "", errors.New("the vault cannot read the assignments, so it cannot tell which node holds the bus")
|
||||
}
|
||||
keys, err := v.Assignments.Keys()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
|
||||
}
|
||||
var nodes []string
|
||||
for _, k := range keys {
|
||||
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
switch len(nodes) {
|
||||
case 1:
|
||||
return nodes[0], nil
|
||||
case 0:
|
||||
return "", fmt.Errorf("no node is assigned the bus module %s", BusModule)
|
||||
}
|
||||
return "", fmt.Errorf("%d nodes are assigned the bus module %s (%s): the vault adopts from one alone", len(nodes),
|
||||
BusModule, strings.Join(nodes, ", "))
|
||||
}
|
||||
|
||||
// AdoptNow is the tool account_adopt: the bus node's have read now and adopted, though it says the same as before.
|
||||
func (v *Vault) AdoptNow(caller string) (Adoption, error) {
|
||||
if v.Haves == nil {
|
||||
|
||||
@@ -54,7 +54,7 @@ func build() *Env {
|
||||
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: assigned.Read, Confirm: readAssignment, Store: store},
|
||||
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant,
|
||||
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }}
|
||||
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement}
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -139,6 +139,18 @@ func watchWants(env *Env) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// placement reads the indexer's placement through the runtime under the vault's own account (its manifest reads it).
|
||||
var placement = PlacementReader{
|
||||
Keys: func() ([]string, error) { return stdio.State(PlacementRead).Keys() },
|
||||
Read: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(PlacementRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
},
|
||||
}
|
||||
|
||||
// readWant is one wanted account of the grants part, read afresh (issue-account).
|
||||
func readWant(user string) ([]byte, bool, error) {
|
||||
e, err := stdio.State(WantedRead).Get(user)
|
||||
|
||||
@@ -84,22 +84,31 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead
|
||||
if c.Consumer != node {
|
||||
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
|
||||
}
|
||||
// The consuming module stands on the machine: read, never assumed (an assignment that cannot be read refuses).
|
||||
// The consuming module stands on the machine: read from the placement and the assignments, never assumed (one
|
||||
// that cannot be read refuses). The assignments' mirror can be behind, so one it does not hold is confirmed directly.
|
||||
placed, err := v.placedOn(node, c.ConsumerModule)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"where %s stands could not be read: %v", c.ConsumerModule, err)
|
||||
}
|
||||
if placed {
|
||||
return ""
|
||||
}
|
||||
raw, why, err := plan.read(c.ConsumerModule)
|
||||
if why != "" {
|
||||
if why != "" && v.Placement.Read == nil {
|
||||
return why
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
||||
}
|
||||
if raw == nil {
|
||||
// Confirmed directly before refusing: the mirror may not hold a module assigned a moment ago.
|
||||
if raw, err = plan.confirmed(c.ConsumerModule); err != nil {
|
||||
if why == "" {
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
||||
}
|
||||
if raw == nil {
|
||||
if raw, err = plan.confirmed(c.ConsumerModule); err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw == nil {
|
||||
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
||||
return fmt.Sprintf("%s is not on %s: neither placed nor assigned there, nor required by a module there", c.ConsumerModule, node)
|
||||
}
|
||||
return ""
|
||||
case "provider":
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
// Where modules stand, from the catalogue's placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774). The controller's
|
||||
// assignments mirror goes with its lease, so the vault reads which module stands on which node from the indexer's
|
||||
// placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
|
||||
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
|
||||
// its own (issue 774).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// PlacementRead is the indexer's placement, as the vault's manifest reads it.
|
||||
const PlacementRead = delivery.IndexerModule + "." + delivery.PlacementState
|
||||
|
||||
// PlacementReader reads the indexer's placement: every key, and one key's value (nil when there is none).
|
||||
type PlacementReader struct {
|
||||
Keys func() ([]string, error)
|
||||
Read func(key string) ([]byte, error)
|
||||
}
|
||||
|
||||
// placements is every node's placement, by node; none when the vault reads no placement.
|
||||
func (v *Vault) placements() (map[string]delivery.Placement, error) {
|
||||
out := map[string]delivery.Placement{}
|
||||
if v.Placement.Keys == nil || v.Placement.Read == nil {
|
||||
return out, nil
|
||||
}
|
||||
keys, err := v.Placement.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the placement's keys could not be read: %w", err)
|
||||
}
|
||||
for _, k := range keys {
|
||||
node, err := delivery.ParsePlacementKey(k)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
raw, err := v.Placement.Read(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the placement of %s could not be read: %w", node, err)
|
||||
}
|
||||
if raw == nil {
|
||||
continue
|
||||
}
|
||||
p, err := delivery.DecodePlacement(raw)
|
||||
if err != nil || p.Node != node {
|
||||
continue
|
||||
}
|
||||
out[node] = p
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// placedOn says module is placed on node by the catalogue.
|
||||
func (v *Vault) placedOn(node, module string) (bool, error) {
|
||||
if v.Placement.Read == nil {
|
||||
return false, nil
|
||||
}
|
||||
key, err := delivery.PlacementKey(node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
raw, err := v.Placement.Read(key)
|
||||
if err != nil || raw == nil {
|
||||
return false, err
|
||||
}
|
||||
p, err := delivery.DecodePlacement(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
|
||||
}
|
||||
return slices.Contains(p.Assigned(), module), nil
|
||||
}
|
||||
|
||||
// standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is
|
||||
// an error, never "not there".
|
||||
func (v *Vault) standsOn(node, module string) (bool, error) {
|
||||
placed, err := v.placedOn(node, module)
|
||||
if err != nil || placed {
|
||||
return placed, err
|
||||
}
|
||||
d, ok := v.Askers.(Declared)
|
||||
if !ok || d.Read == nil {
|
||||
if v.Placement.Read == nil {
|
||||
return false, errors.New("the vault reads neither the placement nor the assignments")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
raw, err := standing(d.Read)(node + "." + module)
|
||||
return raw != nil, err
|
||||
}
|
||||
|
||||
// busNode is the node running the bus's module: from the placement, and from the assignments while no placement names
|
||||
// one. The vault adopts from one node alone.
|
||||
func (v *Vault) busNode() (string, error) {
|
||||
placed, err := v.placements()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var nodes []string
|
||||
for node, p := range placed {
|
||||
if slices.Contains(p.Assigned(), BusModule) {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
from := "placed"
|
||||
if len(nodes) == 0 && v.Assignments.Keys != nil {
|
||||
from = "assigned"
|
||||
keys, err := v.Assignments.Keys()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
|
||||
}
|
||||
for _, k := range keys {
|
||||
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
} else if len(nodes) == 0 && len(placed) == 0 {
|
||||
return "", errors.New("the vault reads neither the placement nor the assignments, so it cannot tell which node holds the bus")
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
switch len(nodes) {
|
||||
case 1:
|
||||
return nodes[0], nil
|
||||
case 0:
|
||||
return "", fmt.Errorf("no node is %s the bus module %s", from, BusModule)
|
||||
}
|
||||
return "", fmt.Errorf("%d nodes are %s the bus module %s (%s): the vault adopts from one alone", len(nodes), from,
|
||||
BusModule, strings.Join(nodes, ", "))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// placementsOf is the indexer's placement of each node, as the vault reads it.
|
||||
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
|
||||
t.Helper()
|
||||
values := map[string][]byte{}
|
||||
for node, modules := range nodes {
|
||||
p := delivery.Placement{V: delivery.Version, Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
||||
}
|
||||
raw, err := delivery.EncodePlacement(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, _ := delivery.PlacementKey(node)
|
||||
values[key] = raw
|
||||
}
|
||||
return PlacementReader{
|
||||
Keys: func() ([]string, error) {
|
||||
var out []string
|
||||
for k := range values {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
Read: func(key string) ([]byte, error) { return values[key], nil },
|
||||
}
|
||||
}
|
||||
|
||||
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the catalogue's
|
||||
// placement which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
|
||||
// either names stands.
|
||||
func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats", "web"}, "ace": {"shop"}})
|
||||
if node, err := v.busNode(); err != nil || node != "novox" {
|
||||
t.Errorf("the bus node from placement: %q, %v", node, err)
|
||||
}
|
||||
asked := func(node, module string) CredentialAsked {
|
||||
return CredentialAsked{Role: "consumer", Provision: "postgres", Consumer: node, ConsumerModule: module, Provider: "novox"}
|
||||
}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "shop")); why != "" {
|
||||
t.Errorf("shop stands on ace by placement, refused: %s", why)
|
||||
}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "web")); !strings.Contains(why, "is not on ace") {
|
||||
t.Errorf("web is not placed on ace, and is answered %q", why)
|
||||
}
|
||||
// While both stand, an assignment still names a module placement does not yet.
|
||||
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "web")); why != "" {
|
||||
t.Errorf("web assigned on ace, refused: %s", why)
|
||||
}
|
||||
// Two nodes placed with the bus: the vault adopts from neither.
|
||||
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "ace": {"nats"}})
|
||||
if _, err := v.busNode(); err == nil || !strings.Contains(err.Error(), "2 nodes") {
|
||||
t.Errorf("two bus nodes: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@ type Vault struct {
|
||||
adoption adoptionKept
|
||||
// WantRead reads one wanted account afresh (issue-account): nil reads the wants as the watch handed them.
|
||||
WantRead func(user string) ([]byte, bool, error)
|
||||
// Placement reads the catalogue's placement (placed.go): where modules stand once the assignments are gone.
|
||||
Placement PlacementReader
|
||||
// LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it.
|
||||
LeaseRead func() ([]byte, bool, error)
|
||||
// minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go).
|
||||
|
||||
@@ -131,10 +131,11 @@ func filesUnder(t *testing.T, dir string) map[string]string {
|
||||
|
||||
// The manifest declares one bucket, and no secret is in it: the vault's only place for a secret is its own store on its
|
||||
// own disk (ADR 0349 §7, ADR 0201 §13). The one bucket is bus-accounts, the bcrypt hashes of the bus accounts it holds
|
||||
// (ADR 0547 clause 9), opened in accounts.go alone. It reads four buckets of others', and only reads them: the
|
||||
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
|
||||
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), the node-engines' haves, where
|
||||
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
|
||||
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), in main.go alone.
|
||||
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the catalogue's placement (placed.go), in
|
||||
// main.go alone.
|
||||
func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
@@ -148,7 +149,7 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the one "+
|
||||
"bucket is the accounts' hashes (ADR 0547 clause 9)", s)
|
||||
}
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease"]` {
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-indexer.placement"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the controller's assignments, the haves and the grants part's "+
|
||||
"wanted accounts alone", r)
|
||||
}
|
||||
@@ -183,6 +184,9 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
// And the controller's lease, one read and one watch (ADR 0561 §3).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1))
|
||||
// And the catalogue's placement, where modules stand (placed.go): its keys and one key, read.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 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)
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"mesh-controller.assignments",
|
||||
"node-engine.have",
|
||||
"mesh-grants.accounts",
|
||||
"mesh-controller.lease"
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-indexer.placement"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user