Merge pull request 'Node-engine: a held provisioner client keeps its account; a module waiting for a yes has its new build read (issues 774, 1585)' (#204) from fix/frombus-inown-build into main
This commit was merged in pull request #204.
This commit is contained in:
@@ -509,6 +509,12 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
// composes its own (issue 774).
|
||||
Applied: appliedStoreFor(opts.state),
|
||||
Inactive: inactiveIn(&liveness.Exec{Run: liveness.Runner(opts.run()), System: builtFor}, heldBy(apply.WindowsIn(filepath.Dir(opts.state)))),
|
||||
Held: func() map[string]string {
|
||||
if c := composerNow.Load(); c != nil {
|
||||
return c.LastHeld()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Revisions: func(ctx context.Context, key string) ([]agent.Update, error) {
|
||||
got, err := session.KVHistory(ctx, composedBucket, key)
|
||||
if err != nil {
|
||||
|
||||
@@ -136,6 +136,9 @@ type Env struct {
|
||||
// Revisions are a composition key's revisions the bucket keeps, oldest first; nil reads none. The composition a want
|
||||
// names is taken from them when a newer one replaced it under the same key (ADR 0539 clause 1).
|
||||
Revisions func(ctx context.Context, key string) ([]Update, error)
|
||||
// Held is what the node's composer held in its last pass, each module with why, as the verb composition shows it
|
||||
// for the node's own (issue 774); nil says nothing.
|
||||
Held func() map[string]string
|
||||
// Settled is told a module's apply or removal that succeeded, once what runs holds it (Everything), outside the
|
||||
// agent's lock: the declaration the node applies now names what that composition names, its filled files among
|
||||
// them. The node-engine's filling tells the provisioner's client then to fill again what it refused (mesh-host
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -1312,6 +1313,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
defer historyMu.Unlock()
|
||||
history[key] = append(history[key], Update{Key: key, Value: raw, Revision: uint64(len(history[key]) + 1)})
|
||||
}
|
||||
b.env.Held = func() map[string]string { return map[string]string{"mesh-build-go": "its binding is unset"} }
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
|
||||
@@ -1493,6 +1495,16 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
if _, err := b.ag.Composition(context.Background(), "db", "sha256:"+strings.Repeat("0", 64), false); err == nil {
|
||||
t.Error("a digest no revision has was answered")
|
||||
}
|
||||
// Issue 774 (ace, 2026-11-04 01:29): the node's own revision shows its own fields, the provisioner client's account
|
||||
// and the adoption among them, and what the composer's last pass held.
|
||||
ownKey, ownRaw, ownDig := encodedAs(t, delivery.Composed{Node: node, Own: delivery.NodeOwn{Provisioner: "mesh-provisioner-client",
|
||||
Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := delivery.ComposedNodeKey(node); return k }())
|
||||
keep(ownKey, ownRaw)
|
||||
if view, err := b.ag.Composition(context.Background(), NodeModule, ownDig, false); err != nil || view.Own == nil ||
|
||||
view.Own.Provisioner != "mesh-provisioner-client" || view.Own.Adoption == nil || view.Own.Adoption.Taken[0] != "web" ||
|
||||
view.Own.Held["mesh-build-go"] == "" {
|
||||
t.Errorf("the node's own composition reads %+v %+v (%v)", view, view.Own, err)
|
||||
}
|
||||
// novox/hq ADR 0593 §6: the engine publishes each composition with its plan in its composition state; the verb reads
|
||||
// that revision by the digest its have and want name, its resources' (the plan left out of the digest).
|
||||
planned, _ := delivery.DecodeComposed(draw)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/judge"
|
||||
)
|
||||
@@ -32,6 +33,26 @@ type CompositionView struct {
|
||||
Judged JudgedView `json:"judged"`
|
||||
// Diff is every resource that differs from what runs, when asked.
|
||||
Diff []judge.ResourceChange `json:"diff,omitempty"`
|
||||
// Own is, of the node's own composition (module node), its own fields beside what runs names (issue 774: ace's
|
||||
// own composition dropped the provisioner client's account, and nothing read could show it).
|
||||
Own *OwnView `json:"own,omitempty"`
|
||||
}
|
||||
|
||||
// OwnView is the node's own fields of a composition (delivery.NodeOwn) beside what runs of them, and what the node's
|
||||
// composer held in its last pass.
|
||||
type OwnView struct {
|
||||
// Provisioner is the account the provisioner's client runs as, as the revision read names it; RunsProvisioner as
|
||||
// what runs names it. Empty: the declaration names none, and every filled write is refused.
|
||||
Provisioner string `json:"provisioner_account"`
|
||||
RunsProvisioner string `json:"runs_provisioner_account,omitempty"`
|
||||
// Adoption is the revision's adoption: the node adopted, and which of its modules are taken; nil for a converged node.
|
||||
Adoption *declaration.Adoption `json:"adoption,omitempty"`
|
||||
// Identities, Groups and Libraries count what the revision names of each.
|
||||
Identities int `json:"identities,omitempty"`
|
||||
Groups int `json:"groups,omitempty"`
|
||||
Libraries int `json:"libraries,omitempty"`
|
||||
// Held is each module the node's composer held in its last pass, with why.
|
||||
Held map[string]string `json:"held,omitempty"`
|
||||
}
|
||||
|
||||
// WantView is a want as the verb shows it.
|
||||
@@ -171,6 +192,17 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
|
||||
}
|
||||
view.Runs = rv
|
||||
}
|
||||
if module == NodeModule {
|
||||
o := read.C.Own
|
||||
view.Own = &OwnView{Provisioner: o.Provisioner, Adoption: o.Adoption, Identities: len(o.Identities), Groups: len(o.Groups),
|
||||
Libraries: len(o.Libraries)}
|
||||
if running != nil {
|
||||
view.Own.RunsProvisioner = running.Own.Provisioner
|
||||
}
|
||||
if a.env.Held != nil {
|
||||
view.Own.Held = a.env.Held()
|
||||
}
|
||||
}
|
||||
j := judge.JudgeMove(running, read.C)
|
||||
view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words}
|
||||
if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) {
|
||||
|
||||
@@ -63,6 +63,15 @@ func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (ou
|
||||
return compositions(in, node, fps, mode, false)
|
||||
}
|
||||
|
||||
// Folds says whether a module the node runs inside its own composition (inOwn, Node.InOwn) is folded there at the
|
||||
// fingerprint fp it is composed at: only at no fingerprint of the new form. At one (src2:) it has a composition of its
|
||||
// own, composed from its build, so every reader of a build (frombus) takes the build in exactly where this says it is
|
||||
// not folded (issue 1585: nats on novox, folded by the reader and composed at src2: here, refused for its server
|
||||
// artifact no build had published).
|
||||
func Folds(inOwn bool, fp string) bool {
|
||||
return inOwn && !strings.HasPrefix(fp, delivery.SourcePrefix2)
|
||||
}
|
||||
|
||||
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped, stood []string, err error) {
|
||||
reading := in
|
||||
reading.Nodes = append([]Node(nil), in.Nodes...)
|
||||
@@ -84,7 +93,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
||||
// The private network's module is built by nobody: no fingerprint of the new form ever names it.
|
||||
inOwn[overlay.Name] = true
|
||||
folds := func(module string) bool {
|
||||
return module != "" && inOwn[module] && !strings.HasPrefix(fps[module], delivery.SourcePrefix2)
|
||||
return module != "" && Folds(inOwn[module], fps[module])
|
||||
}
|
||||
if !found {
|
||||
return nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
|
||||
|
||||
@@ -191,6 +191,28 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
!strings.Contains(held["spiegel"], "spiegel has not been built for this mesh") {
|
||||
t.Errorf("composed spiegel without what runs: %v %v, want spiegel held alone", err, held)
|
||||
}
|
||||
// Issue 1585 (nats on novox, met twice): wanted at a build of the new form, a module in InOwn is no longer folded
|
||||
// (compose.Folds), so its build is taken in and it composes with its image, never from the indexer's manifest as
|
||||
// "not built for this mesh".
|
||||
{
|
||||
bb := mk("lemurs", overlay.Name, "spiegel")
|
||||
bp := bb.placements["n1"]
|
||||
bp.Decided.Endpoint = "n1.example:51820"
|
||||
bb.placements["n1"] = bp
|
||||
sp := bb.modules["spiegel"]
|
||||
sp.FP2 = fp("3")
|
||||
bb.modules["spiegel"] = sp
|
||||
image := "registry.example:5100/spiegel/site@sha256:" + strings.Repeat("7", 64)
|
||||
bb.builds["spiegel "+sp.FP2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: sp.Manifest,
|
||||
Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}}
|
||||
read, err := ComposingInput(ctx, bb, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}},
|
||||
map[string]string{"lemurs": fp("e"), "spiegel": sp.FP2})
|
||||
raw, _ := json.Marshal(read.Catalogue["spiegel"])
|
||||
if err != nil || read.ComposedHow.Held["spiegel"] != "" || !strings.Contains(string(raw), strings.Repeat("7", 64)) {
|
||||
t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.FP2, err,
|
||||
read.ComposedHow, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// (C) Issue 1126, C5: what the others read of a module carried as it runs is what runs, never its stub's: the filter
|
||||
// opens the machine port its running container publishes (de-spiegel's 20005 on novox, not its software's 8080; a
|
||||
|
||||
@@ -234,6 +234,7 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
||||
}
|
||||
in, err := b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
|
||||
if err == nil {
|
||||
runningProvisioner(&in, s.Node, s.Running)
|
||||
// What runs is kept while a manifest does not yet say what shared code no longer infers (ADR 0601 §4).
|
||||
for _, k := range keepWhatRuns(&in, s.Node, s.Running) {
|
||||
if s.Kept != nil {
|
||||
@@ -876,7 +877,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
|
||||
keptAt := maps.Clone(s.KeptAt)
|
||||
for _, pm := range p.Modules {
|
||||
if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 &&
|
||||
keptAt[pm.Module] == "" && !slices.Contains(s.InOwn, pm.Module) {
|
||||
keptAt[pm.Module] == "" && !compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
|
||||
if keptAt == nil {
|
||||
keptAt = map[string]string{}
|
||||
}
|
||||
@@ -891,9 +892,10 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
|
||||
}
|
||||
for _, pm := range p.Modules {
|
||||
m, ok := modules[pm.Module]
|
||||
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
|
||||
// A module it runs inside its own composition is composed there as before: no build of the new form is what
|
||||
// it runs (Scope.InOwn).
|
||||
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
|
||||
// A module it runs inside its own composition is composed there as before while it is folded: no build of the
|
||||
// new form is what it runs (Scope.InOwn). Wanted at a build of the new form it is no longer folded (compose.Folds,
|
||||
// the composition's own rule), so its build is taken in (issue 1585).
|
||||
continue
|
||||
}
|
||||
if _, refused := in.Refused[pm.Module]; refused {
|
||||
@@ -1230,3 +1232,38 @@ func withoutHeldData(in compose.Input, node string) compose.Input {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// runningProvisioner names, on node, the account its provisioner client runs as now (compose.Node.RunningProvisioner): the
|
||||
// account its own composition as it runs names, else the user of the running process of the module that runs the client.
|
||||
func runningProvisioner(in *compose.Input, node string, running []delivery.Composed) {
|
||||
account := ""
|
||||
for _, c := range running {
|
||||
if c.Module == "" && c.Own.Provisioner != "" {
|
||||
account = c.Own.Provisioner
|
||||
}
|
||||
}
|
||||
for _, c := range running {
|
||||
if account != "" {
|
||||
break
|
||||
}
|
||||
man, ok := in.Catalogue[c.Module]
|
||||
if c.Module == "" || !ok || !man.ClaimsSeat(compose.NodeProvisionerSeat) {
|
||||
continue
|
||||
}
|
||||
for _, raw := range c.Resources {
|
||||
var r struct {
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
if json.Unmarshal(raw, &r) == nil && r.Type == "process" && r.User != "" && r.User != "root" {
|
||||
account = r.User
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range in.Nodes {
|
||||
if in.Nodes[i].Name == node {
|
||||
in.Nodes[i].RunningProvisioner = account
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package frombustest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -204,6 +206,83 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
if fetched-archives != 1 {
|
||||
t.Errorf("restic's archive was read %d times over two compositions, want once", fetched-archives)
|
||||
}
|
||||
|
||||
// Issue 774 (ace, 2026-11-04 01:29): the provisioner client held as its stub (its build not done, so the node keeps
|
||||
// running what it runs) keeps the account the node's own composition names, and restic stays handed to it with its
|
||||
// plan; without it every filled write was refused and no module was handed.
|
||||
if b, err = Provisioned(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fps, ownKey := FPs(), ""
|
||||
ownKey, _ = delivery.ComposedNodeKey(Node)
|
||||
resticKey, _ := delivery.ComposedKey(Node, "restic", fps["restic"])
|
||||
before, _, _, err := frombus.Compose(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}, fps)
|
||||
account := before[ownKey].Own.Provisioner
|
||||
if err != nil || account == "" || len(before[resticKey].Filled) == 0 {
|
||||
t.Fatalf("the node composes with its client: account %q, restic's filled files %v: %v", account, before[resticKey].Filled, err)
|
||||
}
|
||||
var running []delivery.Composed
|
||||
for _, c := range before {
|
||||
running = append(running, c)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
own, placed bool // the account read from the running own composition, else the client's running process; the client placed
|
||||
userless bool // the client placed and not held, its process naming no user: it composes no account
|
||||
}{{true, true, false}, {false, true, false}, {true, false, false}, {false, false, false}, {true, true, true}} {
|
||||
own := row.own
|
||||
runs := running
|
||||
if !own { // what runs names no account of its own: the client's running process says it
|
||||
runs = nil
|
||||
for _, c := range running {
|
||||
if c.Module == "" {
|
||||
c.Own.Provisioner = ""
|
||||
}
|
||||
runs = append(runs, c)
|
||||
}
|
||||
}
|
||||
bb, err := Provisioned()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := account
|
||||
if row.userless {
|
||||
// placed and not held, composing no account of its own: no stale account is kept from what runs
|
||||
mod := bb.Module["mesh-provisioner-client"]
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(bb.Manifests[mod.Manifest], &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range m["resources"].([]any) {
|
||||
delete(r.(map[string]any), "user")
|
||||
}
|
||||
raw, _ := json.Marshal(m)
|
||||
mod.Manifest = delivery.ManifestDigest(raw)
|
||||
bb.Module["mesh-provisioner-client"], bb.Manifests[mod.Manifest] = mod, raw
|
||||
built := *bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]]
|
||||
built.Manifest = mod.Manifest
|
||||
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &built
|
||||
want = ""
|
||||
} else if row.placed {
|
||||
// held: its build not done while it runs
|
||||
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding}
|
||||
} else {
|
||||
// taken out of the node's placement while what runs still names it: the account goes with it, never kept stale
|
||||
p := bb.Placement[Node]
|
||||
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m delivery.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
|
||||
bb.Placement[Node] = p
|
||||
want = ""
|
||||
}
|
||||
after, _, held, err := frombus.Compose(context.Background(), bb, frombus.Mesh{}, frombus.Scope{Node: Node, Running: runs}, fps)
|
||||
if _, isHeld := held["mesh-provisioner-client"]; err != nil || isHeld != (row.placed && !row.userless) {
|
||||
t.Fatalf("%+v: held %v: %v", row, held, err)
|
||||
}
|
||||
if got := after[ownKey].Own.Provisioner; got != want {
|
||||
t.Errorf("%+v: the node's own names %q, want %q", row, got, want)
|
||||
}
|
||||
if row.placed && !row.userless && !reflect.DeepEqual(after[resticKey].Filled, before[resticKey].Filled) {
|
||||
t.Errorf("%+v: restic's filled files %v, want %v", row, after[resticKey].Filled, before[resticKey].Filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// statesOf is the bus b as a module's read states hold it.
|
||||
|
||||
@@ -224,6 +224,11 @@ type Node struct {
|
||||
// so it is left to the node: Compositions composes it as a stub (Stub) and carries its resources as they run, each
|
||||
// after the resource it runs after (issue 1126). Nil composes every folded module from its manifest, as before.
|
||||
RunsOwn *delivery.Composed
|
||||
// RunningProvisioner is the account the provisioner's client runs as on this node now, read from what runs (its own
|
||||
// composition's provisioner account, else the user of the provisioner client's running process), empty when nothing
|
||||
// says it. A client held as its stub this pass composes no process, so the node keeps this account and its handing
|
||||
// (provisionerAccount; issue 774: ace, 2026-11-04 01:29, every filled write refused and postgres's consumers unhanded).
|
||||
RunningProvisioner string
|
||||
// Held is the modules refused alone on this node, each composed as what runs of it (Carried, in Kept; its Stub when
|
||||
// nothing of it runs) so the rest is composed (novox/hq issue 1126): none of a held module's own composition is in what
|
||||
// Compositions answers, so the node keeps what it last applied of it; one whose resources the node's own runs
|
||||
|
||||
@@ -74,6 +74,12 @@ func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) {
|
||||
// not sent the field they would refuse.
|
||||
r.Hands = takesHanded(node)
|
||||
r.Provisioner = provisionerAccount(plan)
|
||||
if r.Provisioner == "" && provisionerHeld(plan) {
|
||||
// The provisioner's client is placed here and held this pass (its stub, or carried as it runs, composes no process
|
||||
// of its own): it keeps running as it runs, so the account it runs as stays named, and every module stays handed
|
||||
// to it. A client not held that composes no account, or none placed, names none: no stale account is kept.
|
||||
r.Provisioner = node.RunningProvisioner
|
||||
}
|
||||
r.Assigned = node.Handed
|
||||
r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract
|
||||
r.JudgesRoot = node.Stated && node.Contract >= RootContract
|
||||
@@ -190,6 +196,21 @@ func provisionerAccount(plan catalogue.Resolution) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// provisionerHeld says the plan places a module claiming the provisioner client's seat on the machine that this pass
|
||||
// holds (its stand-in: Stub, or Carried as it runs), which composes none of the client's process.
|
||||
func provisionerHeld(plan catalogue.Resolution) bool {
|
||||
for _, m := range plan.Modules {
|
||||
if m.Stubbed && m.ClaimsSeat(NodeProvisionerSeat) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RunsTheProvisionerClient says a module runs its machine's provisioner client (runsTheProvisionerClient), for a reader
|
||||
// of what runs.
|
||||
func RunsTheProvisionerClient(m catalogue.Manifest) bool { return runsTheProvisionerClient(m) }
|
||||
|
||||
// runsTheProvisionerClient says a module runs its machine's provisioner client: it claims the seat and declares the own
|
||||
// secret that account is delivered in.
|
||||
func runsTheProvisionerClient(m catalogue.Manifest) bool {
|
||||
|
||||
@@ -218,6 +218,13 @@ type Pass struct {
|
||||
Earlier bool `json:"earlier,omitempty"`
|
||||
}
|
||||
|
||||
// LastHeld is what the last pass held, each module with why; nil before a pass or when it held none.
|
||||
func (c *Composer) LastHeld() map[string]string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return maps.Clone(c.last.Held)
|
||||
}
|
||||
|
||||
// askedWithin is how recent a pass answers an ask of the verb compose without a new one: the first wait after a seat
|
||||
// that did not answer (laterAgain's first).
|
||||
var askedWithin = 5 * time.Second
|
||||
@@ -915,6 +922,13 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
}
|
||||
c.paid()
|
||||
pass.Held = held
|
||||
// The provisioner client's account the node runs with, dropped by this pass, is said: every filled write is refused
|
||||
// under a declaration that names none (filling.JudgeAccount), and no module is handed to it (issue 774, ace,
|
||||
// 2026-11-04 01:29). A placed client keeps its account while it is held (compose.Node.RunningProvisioner).
|
||||
if was, now := ownProvisioner(running), ownProvisioner(mapValues(compositions)); was != "" && now == "" && nodeWanted {
|
||||
c.sayOnceEach(fmt.Sprintf("the node's own composition names no provisioner client's account, where what runs names %s: "+
|
||||
"once applied, every filled write is refused and no module is handed to the provisioner", was))
|
||||
}
|
||||
// A module refused alone is held: it keeps what it runs, its have says why, and every other module is applied (issue
|
||||
// 1126: one module's refusal froze the whole node).
|
||||
done := c.waiting("the agent marking each held module's have")
|
||||
@@ -1699,3 +1713,21 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ownProvisioner is the provisioner client's account the node's own composition among cs names, or "".
|
||||
func ownProvisioner(cs []delivery.Composed) string {
|
||||
for _, c := range cs {
|
||||
if c.Module == "" {
|
||||
return c.Own.Provisioner
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mapValues(m map[string]delivery.Composed) []delivery.Composed {
|
||||
out := make([]delivery.Composed, 0, len(m))
|
||||
for _, k := range slices.Sorted(maps.Keys(m)) {
|
||||
out = append(out, m[k])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user