diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index eb9659a..68759f5 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -120,7 +120,7 @@ func (h sessionHaves) Write(ctx context.Context, x agent.Have) { } h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: phase, Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits, - Sealed: x.Sealed}) + BuildAlone: x.BuildAlone, Sealed: x.Sealed}) } func (h sessionHaves) Removed(ctx context.Context, module string) { @@ -630,7 +630,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus composer := selfComposer(session, node, hooks, say) machinesNow.Store(composer) // the machines a failing network part points at, by the placements (issue 774) composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone - env.Composed, env.SelfComposed, env.Policy = composer, true, composer.Policy + env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere") ag = agent.New(env) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index c392947..35d5d42 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -58,6 +58,8 @@ type Have struct { // waits for a person's yes (ADR 0547 §3). Judgement string Waits string + // BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone). + BuildAlone bool // Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by // slot (delivery.Have.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at // its own slot alone (issue 1126, C1). @@ -153,6 +155,11 @@ type Env struct { // move of a module of policy record that recreates a container or moves its data waits for a person's yes in its // want (ADR 0547 §3). Nil judges no move. Policy func(ctx context.Context, module string) string + // ComposedToday is the digest of a module composed at the build fp with today's settings and secrets, as the node would + // compose it now, and whether it could be composed: a move to another build whose composition of the build it runs is + // what runs changes the build alone (novox/hq ADR 0601 §1), and the have says so. Nil: no move is said to change the + // build alone, and each is asked of a person. + ComposedToday func(ctx context.Context, module, fp string) (string, bool) // HoldsData says whether a module's manifest declares data (catalogue.Manifest.DataItems), and whether its manifest // could be read: what a composition's Data is, for what runs of a module from the kept declaration, which carries no // Data of its own. A composition says Data, so without it a data module from the kept declaration is judged to move @@ -1649,10 +1656,27 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp prev := a.running[module] a.mu.Unlock() a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseWaitingYes), - Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig}) + Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)}) return true } +// buildAlone says a move of module from what runs to the build want changes the build alone (ADR 0601 §1): another build than +// the one it runs, and the build it runs composed with today's settings and secrets is the composition it runs. Anything it +// cannot tell is no: the move is then asked of a person. +func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Have, want string) bool { + if prev.FP == "" && prev.Composed == "" { + // Nothing of it run since this agent's start: its last have says what runs, as the have written keeps it. + if h, ok := a.env.Haves.Read(ctx, module); ok { + prev.FP, prev.Composed = h.FP, h.Composed + } + } + if a.env.ComposedToday == nil || prev.FP == "" || prev.Composed == "" || prev.FP == want { + return false + } + dig, ok := a.env.ComposedToday(ctx, module, prev.FP) + return ok && dig == prev.Composed +} + // runningComposition is what the module runs now: the composition last applied, else its resources in the declaration // the node kept from before the switch. Called with a.mu held. // diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index f2b3306..a6e7b2f 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -1288,6 +1288,17 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( } return catalogue.PolicyRoll } + // ADR 0601 §1: what runs composed today. rec's build composed again is what it runs, so its move changes the build alone; + // db's is not (a setting changed too). + b.env.ComposedToday = func(_ context.Context, m, fp string) (string, bool) { + switch m { + case "rec": + return "sha256:" + strings.Repeat("8", 64), fp == fpOf("d") + case "db": + return "sha256:" + strings.Repeat("6", 64), true + } + return "", false + } // The bucket's revisions of each key, as the verb composition reads them (issue 774). var historyMu sync.Mutex history := map[string][]Update{} @@ -1452,6 +1463,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( h := b.haves.last("db") return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == ddigMoved }) + if h := b.haves.last("db"); h.BuildAlone { + t.Errorf("a revision at the build it runs is said to change the build alone: %+v", h) + } // The verb composition says why it waits: the revision that waits, what runs, the judgement, and with diff the // container it recreates; the revision that runs, asked by its digest, is no move (issue 774). keep(dk, draw) @@ -1508,7 +1522,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( for _, row := range []struct { module, fp, composed, waits string sealed delivery.Seals - }{{"db", fp, "", ddig2, nil}, {"rec", recFP, recDig, rdig, delivery.Seals{"rec.key#sealed": `{"box":"rec"}`}}} { + alone bool + }{{"db", fp, "", ddig2, nil, false}, {"rec", recFP, recDig, rdig, delivery.Seals{"rec.key#sealed": `{"box":"rec"}`}, true}} { eventually(t, row.module+"'s move did not wait for a yes", func() bool { h := b.haves.last(row.module) return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == row.waits @@ -1518,6 +1533,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( (row.sealed != nil && !maps.Equal(h.Sealed, row.sealed)) { t.Errorf("%s's have waiting for a yes does not keep what runs: %+v", row.module, h) } + if h.BuildAlone != row.alone { + t.Errorf("%s's have waiting for a yes says build alone %v, want %v", row.module, h.BuildAlone, row.alone) + } } if got := b.appliedList(); len(got) != applied { t.Fatalf("a move waiting for a yes was applied: %v", got) diff --git a/internal/link/have.go b/internal/link/have.go index 6f8040a..7f94aa6 100644 --- a/internal/link/have.go +++ b/internal/link/have.go @@ -375,7 +375,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { h := delivery.Have{V: delivery.Version, FP: f.FP, Composed: f.Composed, Build: f.Build, Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail} if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes { - h.Judgement, h.Waits = delivery.Judgement(f.Judgement), f.Waits + h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone } switch { case f.Phase != "": diff --git a/internal/link/messages.go b/internal/link/messages.go index 531a1ab..05ec57a 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -639,6 +639,8 @@ type ModuleFact struct { // Judgement and Waits are, in phase waiting-yes, the judgement of the move and the composition waiting (ADR 0547 §3). Judgement string Waits string + // BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, delivery.Have.BuildAlone). + BuildAlone bool // Sealed is the values the composition it names carries sealed to this node's key, by slot (delivery.Have.SealedAt). Sealed delivery.Seals } diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index a539ad5..8726b82 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -1635,3 +1635,48 @@ func Transient(err error) bool { } return false } + +// ComposedToday is the digest of module composed at the build fp with today's settings and secrets, as this node would +// compose it now beside its other modules at their wanted builds, the boxes it runs kept as compose keeps them, and whether +// it composed (novox/hq ADR 0601 §1): the node agent compares it with what runs, to say whether a move to another build +// changes the build alone. A module held, a node refused, or no placement: false, and the move is asked of a person. +func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string, bool) { + c.mu.Lock() + fps, kept := map[string]string{}, map[string]string{} + for m, w := range c.wants { + if m == agent.NodeModule { + continue + } + fps[m] = w.FP + if mm, ok := c.modules[m]; ok && mm.FP2 != w.FP { + kept[m] = w.FP + } + } + fps[module] = fp + delete(kept, module) + if mm, ok := c.modules[module]; !ok || mm.FP2 != fp { + kept[module] = fp + } + own, placed := c.placements[c.env.Node] + inOwn := c.inOwnOf(own) + c.mu.Unlock() + if !placed { + return "", false + } + var running []delivery.Composed + if c.env.Running != nil { + running = c.env.Running() + } + scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running} + comps, _, held, err := frombus.Compose(ctx, reader{c: c}, c.env.Mesh, scope, fps) + if err != nil || held[module] != "" { + return "", false + } + for _, comp := range comps { + if comp.Module == module && comp.FP == fp { + dig, err := delivery.ComposedDigestOf(c.keepRunning(comp, running)) + return dig, err == nil + } + } + return "", false +}