Merge pull request 'A module's bus account not issued yet is foreseen in a node's answer, so a new module can be assigned; a refusal leads with each held module's first reason (issue 774)' (#141) from fix/kept-read-and-foreseen-account into main

This commit was merged in pull request #141.
This commit is contained in:
2026-10-30 19:49:12 +00:00
5 changed files with 63 additions and 49 deletions
+4 -26
View File
@@ -257,28 +257,6 @@ func (c *composer) wouldMakeSecretForModule(node Node, module, name string) erro
return nil return nil
} }
// busCredentialIssued refuses an own secret called `broker` whose bus account nobody issued (novox/hq issue 203): the broker
// credential is never invented, an account on the bus minted by `module issue`.
func (c *composer) busCredentialIssued(node, module, name string) error {
var user string
switch name {
case "broker":
user = node + "." + module
case ProvisionerSecret:
user = "provisioner." + node
default:
return nil
}
if !slices.Contains(c.in.BusMinted, user) {
return fmt.Errorf(
"%s on %s has no bus credential: nothing was issued for %s, and a push "+
"would seal a placeholder its process cannot read (novox/hq issue 203). "+
"`module issue %s --node %s`, then push again",
module, node, user, module, node)
}
return nil
}
// givenUnderAnOldKey is the refusal for a value a person gave sealed to a key the machine no longer holds. // givenUnderAnOldKey is the refusal for a value a person gave sealed to a key the machine no longer holds.
func givenUnderAnOldKey(module, node, name string) error { func givenUnderAnOldKey(module, node, name string) error {
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+ return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
@@ -354,10 +332,10 @@ func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing c
} }
if !held && mode == Foreseeing { if !held && mode == Foreseeing {
// Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in, // Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in,
// named; refused by it -- refused here, the same words (novox/hq issue 275). // named; refused by it -- refused here, the same words (novox/hq issue 275). A bus account not yet issued is
if err := c.busCredentialIssued(node.Name, m.Module, name); err != nil { // foreseen, never refused: the vault mints it from the grants part's wanted list once the placement is written
return nil, nil, err // on the operator's Approve (ADR 0561 §1, ADR 0567 §5), so a composition asked ahead of that cannot hold it
} // (issue 774, 2026-10-30: every proposal placing mesh-conditions, mesh-self-check or mesh-healers refused).
if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil { if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil {
return nil, nil, err return nil, nil, err
} }
+20 -22
View File
@@ -116,34 +116,32 @@ func TestAnOutsideSecretIsPlacedOnlyOnceAPersonGaveIt(t *testing.T) {
} }
} }
// 1364/AC3 (the send's refusals, novox/hq issue 203 and 275): in the Foreseeing mode a bus credential nobody issued and a given secret // 1364/AC3 (the send's refusals, novox/hq issue 275): in the Foreseeing mode a given secret under an old key is refused in the
// under an old key are refused in the controller's words; with the credential issued, the secret is foreseen. No secret value is in // controller's words. No secret value is in an error. A bus account not yet issued is foreseen, issued or not (issue 774: the vault
// an error. // mints it once the placement is written on the Approve, ADR 0561 §1, ADR 0567 §5; mesh-self-check's proposal on ace was refused
// "has no bus credential: nothing was issued for ace.mesh-self-check").
func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) { func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) {
broker := catalogue.Manifest{Module: "gate", Version: "1", broker := catalogue.Manifest{Module: "mesh-self-check", Version: "1", RunsAs: "mesh-self-check",
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/gate/broker", Taken: catalogue.TakenAtStart}}} OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/mesh-self-check/broker", Taken: catalogue.TakenAtStart}}}
in := withAssistant(t) in := withAssistant(t)
in.Catalogue["gate"] = broker in.Catalogue["mesh-self-check"] = broker
laptop := nodeOf(t, &in, "laptop") laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "gate") laptop.Assigned = append(laptop.Assigned, "mesh-self-check")
_, err := Render(in, "laptop", Foreseeing) for _, minted := range [][]string{nil, {"laptop.mesh-self-check"}} {
want := "gate on laptop has no bus credential: nothing was issued for laptop.gate, and a push would seal a placeholder " + in.BusMinted = minted
"its process cannot read (novox/hq issue 203). `module issue gate --node laptop`, then push again" got, err := Render(in, "laptop", Foreseeing)
if err == nil || err.Error() != want { if err != nil || !got.Rendering.Foreseen["mesh-self-check"]["broker"] {
t.Fatalf("got %v, want %q", err, want) t.Fatalf("minted %v: the bus account is not foreseen: %v", minted, err)
} }
// Reading refuses nothing for it: the machine is not running it. // Reading refuses nothing for it: the machine is not running it.
if _, err := Render(in, "laptop", Reading); err != nil { if _, err := Render(in, "laptop", Reading); err != nil {
t.Fatalf("Reading refused a secret the machine is not running: %v", err) t.Fatalf("Reading refused a secret the machine is not running: %v", err)
} }
in.BusMinted = []string{"laptop.gate"}
if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["gate"]["broker"] {
t.Errorf("an issued bus credential is not foreseen: %v", got.Rendering.Foreseen)
} }
// A given secret under an old key. // A given secret under an old key.
in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "accepted", SealedTo: "an-old-key", Sealed: secretValue}, in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "accepted", SealedTo: "an-old-key", Sealed: secretValue},
{Node: "laptop", Module: "gate", Name: "broker", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}} {Node: "laptop", Module: "mesh-self-check", Name: "broker", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}}
_, err = Render(in, "laptop", Foreseeing) _, err := Render(in, "laptop", Foreseeing)
wantOld := "assistant on laptop holds \"password\", which was given to the mesh rather than made by it, and laptop has since generated a new " + wantOld := "assistant on laptop holds \"password\", which was given to the mesh rather than made by it, and laptop has since generated a new " +
"sealing key. The mesh cannot make another; issue it again" "sealing key. The mesh cannot make another; issue it again"
if err == nil || err.Error() != wantOld { if err == nil || err.Error() != wantOld {
+18 -1
View File
@@ -335,7 +335,7 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
if err != nil { if err != nil {
module, leave := culprit(in, node, fps, mode, failed, err) module, leave := culprit(in, node, fps, mode, failed, err)
if module == "" { if module == "" {
return nil, nil, nil, err return nil, nil, nil, firstReason(err, failed)
} }
hold(module, err.Error(), leave) hold(module, err.Error(), leave)
continue continue
@@ -373,6 +373,23 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
return out, skipped, stood, nil return out, skipped, stood, nil
} }
// firstReason is err, a refusal no culprit is found for, led by the first reason of each held module it names: a held
// module's stand-in refused later never replaces why it was held (issue 774, 2026-10-30: mesh-self-check on ace held for
// "has no bus credential", then refused as "runs as its own account and declares no own secret broker", its bare stub's
// words, which were all the proposal's refusal said).
func firstReason(err error, failed map[string]string) error {
var named []string
for _, m := range sortedKeys(failed) {
if strings.Contains(err.Error(), m) {
named = append(named, m+" is held: "+failed[m])
}
}
if len(named) == 0 {
return err
}
return fmt.Errorf("%s; its stand-in is then refused: %w", strings.Join(named, "; "), err)
}
// blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The // blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The
// runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is // runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is
// the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it). // the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it).
+1
View File
@@ -142,6 +142,7 @@ func TestInput(t *testing.T) {
t.Run("a node refused whole says what it held", aNodeRefusedWholeSaysWhatItHeld) t.Run("a node refused whole says what it held", aNodeRefusedWholeSaysWhatItHeld)
t.Run("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes) t.Run("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes)
t.Run("a running module waits for its build", aRunningModuleWaitsForItsBuild) t.Run("a running module waits for its build", aRunningModuleWaitsForItsBuild)
t.Run("a held module's first reason leads", aHeldModulesFirstReasonLeads)
t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed) t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed)
ctx := context.Background() ctx := context.Background()
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
+20
View File
@@ -131,3 +131,23 @@ func rawStrings(rs []json.RawMessage) []string {
} }
return out return out
} }
// Issue 774: a refusal of a held module's stand-in leads with why the module was held, its first reason, as mesh-self-check's
// on ace should have; a refusal naming no held module is as it was.
func aHeldModulesFirstReasonLeads(t *testing.T) {
held := map[string]string{"mesh-self-check": "mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check"}
for _, row := range []struct {
err, want string
}{
{"mesh-self-check runs as its own account and declares no own secret broker",
"mesh-self-check is held: mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check; " +
"its stand-in is then refused: mesh-self-check runs as its own account and declares no own secret broker"},
{"ace is the hub and has no endpoint", "ace is the hub and has no endpoint"},
} {
err := errors.New(row.err)
got := firstReason(err, held)
if got.Error() != row.want || !errors.Is(got, err) {
t.Errorf("refused %q, want %q", got, row.want)
}
}
}