mesh-vault: read which modules of a machine the vault judges handed, and why, with own_handed (issue 1966) #606

Merged
mesh-tools merged 1 commits from fix/handed-one-rule into main 2026-11-04 09:16:10 +00:00
4 changed files with 111 additions and 0 deletions
Showing only changes of commit 237a534ca4 - Show all commits
@@ -10,6 +10,7 @@ package main
import (
"errors"
"fmt"
"maps"
"slices"
"sort"
"strings"
@@ -424,3 +425,72 @@ func hands(raw []byte) bool {
h, err := handsOf(raw)
return err == nil && h
}
// HandedJudged is how the vault judges one module of a machine for `own`: handed (its own secrets left out, for the
// provisioner's client to fill from `get`), and why, as the plan in its assignment says it; or why it could not judge.
type HandedJudged struct {
Module string `json:"module"`
Handed bool `json:"handed"`
Why string `json:"why"`
}
// HandedOf is the vault's judgement of each module of node, as `own` would answer it now (issue 1966): every module it
// keeps an own secret of there and every module placed there, sorted. Read from the same plans `own` reads, so what it
// says is what the next answer leaves out. A node composes by its manifest, its build and the machine, and the plans it
// makes are what this reads next: a module named here as not handed for want of a plan is handed by its node's next
// composition, where the machine takes it.
func (v *Vault) HandedOf(node string) ([]HandedJudged, error) {
if !namePart.MatchString(node) {
return nil, fmt.Errorf("%q is not a machine's name", node)
}
all, err := v.Store.ListPrefix("own/" + node + "/")
if err != nil {
return nil, err
}
modules := map[string]bool{}
for _, r := range all {
if parts := strings.Split(r.Name, "/"); len(parts) == 4 {
modules[parts[2]] = true
}
}
plan := v.plansOf(node)
placed, placedErr := plan.placedModules()
for _, m := range placed {
modules[m] = true
}
out := make([]HandedJudged, 0, len(modules))
for _, module := range slices.Sorted(maps.Keys(modules)) {
out = append(out, plan.judged(module))
}
if placedErr != nil && len(out) == 0 {
return nil, fmt.Errorf("the placement of %s could not be read and the vault keeps no own secret there: %w", node, placedErr)
}
return out, nil
}
// judged is handed with its why, as HandedOf says it.
func (p *planReader) judged(module string) HandedJudged {
j := HandedJudged{Module: module}
raw, why, err := p.read(module)
switch {
case why != "":
j.Why = "not judged: " + why
return j
case err != nil:
j.Why = fmt.Sprintf("not judged: the assignment %s.%s could not be read: %v", p.node, module, err)
return j
case raw == nil:
j.Why = "no assignment holds a plan of it yet: its own secrets are answered to its node"
return j
}
hands, err := handsOf(raw)
switch {
case err != nil:
j.Why = fmt.Sprintf("not judged: the assignment %s.%s is not the controller's: %v", p.node, module, err)
case hands:
j.Handed, j.Why = true, "its plan hands it: its own secrets are its provisioner client's, from get"
default:
j.Why = "its plan hands nothing (none, or provisions alone): its own secrets are answered to its node"
}
return j
}
@@ -291,4 +291,27 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
if want := map[string]bool{"grafana": false, "web": true, "db": true}; !reflect.DeepEqual(got.Handed, want) {
t.Errorf("own judged %v handed, want %v", got.Handed, want)
}
// Issue 1966: the vault's judgement is read by a tool, each module with why; one with no plan yet is not handed, for
// want of a plan its node's next composition makes.
if _, err := moveIn(t, v, "node.anchor", MoveOf{Node: "anchor", Module: "builder", Name: "token", Origin: "made",
Generation: "g-1", SealingKey: pub}, "v-builder"); err != nil {
t.Fatal(err)
}
answer, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "own_handed").Run(map[string]any{"node": "anchor"})
if err != nil {
t.Fatal(err)
}
judged := map[string]HandedJudged{}
for _, j := range answer.(map[string]any)["modules"].([]HandedJudged) {
judged[j.Module] = j
}
for module, want := range map[string][2]string{"builder": {"false", "no assignment holds a plan"},
"grafana": {"false", "hands nothing"}, "web": {"true", "its plan hands it"}, "db": {"true", "its plan hands it"}} {
if j := judged[module]; fmt.Sprint(j.Handed) != want[0] || !strings.Contains(j.Why, want[1]) {
t.Errorf("own_handed says %s is %+v, want handed %s, why saying %q", module, j, want[0], want[1])
}
}
if len(judged) != 4 {
t.Errorf("own_handed names %v, want builder, db, grafana and web", judged)
}
}
@@ -399,6 +399,23 @@ func definitions(env *Env) []toolDef {
given, _ := a["fingerprint"].(string)
return verifyAnswer(env, as, given), nil
}},
{Name: "own_handed",
Description: "Which modules of a machine the vault judges handed to the provisioner, as its verb `own` answers that " +
"machine's node-engine now (issue 1966): for every module it keeps an own secret of there and every module " +
"placed there, handed or not and why (its plan hands it, hands nothing, no plan yet, or why it could not " +
"judge). A handed module's own secrets are left out of `own`, for its client to fill from `get`. Never a value.",
Input: map[string]any{"node": str("the machine")},
run: func(_ string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
judged, err := v.HandedOf(arg(a, "node"))
if err != nil {
return nil, err
}
return map[string]any{"node": arg(a, "node"), "modules": judged}, nil
}},
{Name: "secret_handed",
Description: "Who was handed which secret by `get`, and when, and who was refused, newest first, as the " +
"vault's own store records it. Never a value.",
+1
View File
@@ -173,6 +173,7 @@
"secret_holders",
"secret_verify",
"secret_handed",
"own_handed",
"secret_own",
"account_hash",
"account_adopt",