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).
|
// composes its own (issue 774).
|
||||||
Applied: appliedStoreFor(opts.state),
|
Applied: appliedStoreFor(opts.state),
|
||||||
Inactive: inactiveIn(&liveness.Exec{Run: liveness.Runner(opts.run()), System: builtFor}, heldBy(apply.WindowsIn(filepath.Dir(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) {
|
Revisions: func(ctx context.Context, key string) ([]agent.Update, error) {
|
||||||
got, err := session.KVHistory(ctx, composedBucket, key)
|
got, err := session.KVHistory(ctx, composedBucket, key)
|
||||||
if err != nil {
|
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
|
// 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).
|
// 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)
|
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
|
// 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
|
// 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
|
// 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"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1312,6 +1313,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
|||||||
defer historyMu.Unlock()
|
defer historyMu.Unlock()
|
||||||
history[key] = append(history[key], Update{Key: key, Value: raw, Revision: uint64(len(history[key]) + 1)})
|
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()
|
stop := b.run()
|
||||||
defer stop()
|
defer stop()
|
||||||
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
|
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 {
|
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")
|
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
|
// 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).
|
// that revision by the digest its have and want name, its resources' (the plan left out of the digest).
|
||||||
planned, _ := delivery.DecodeComposed(draw)
|
planned, _ := delivery.DecodeComposed(draw)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
"github.com/novox/mesh-host/internal/judge"
|
"github.com/novox/mesh-host/internal/judge"
|
||||||
)
|
)
|
||||||
@@ -32,6 +33,26 @@ type CompositionView struct {
|
|||||||
Judged JudgedView `json:"judged"`
|
Judged JudgedView `json:"judged"`
|
||||||
// Diff is every resource that differs from what runs, when asked.
|
// Diff is every resource that differs from what runs, when asked.
|
||||||
Diff []judge.ResourceChange `json:"diff,omitempty"`
|
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.
|
// 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
|
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)
|
j := judge.JudgeMove(running, read.C)
|
||||||
view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words}
|
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) {
|
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)
|
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) {
|
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 := in
|
||||||
reading.Nodes = append([]Node(nil), in.Nodes...)
|
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.
|
// The private network's module is built by nobody: no fingerprint of the new form ever names it.
|
||||||
inOwn[overlay.Name] = true
|
inOwn[overlay.Name] = true
|
||||||
folds := func(module string) bool {
|
folds := func(module string) bool {
|
||||||
return module != "" && inOwn[module] && !strings.HasPrefix(fps[module], delivery.SourcePrefix2)
|
return module != "" && Folds(inOwn[module], fps[module])
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
|
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") {
|
!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)
|
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
|
// (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
|
// 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)
|
in, err := b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
|
||||||
if err == nil {
|
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).
|
// 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) {
|
for _, k := range keepWhatRuns(&in, s.Node, s.Running) {
|
||||||
if s.Kept != nil {
|
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)
|
keptAt := maps.Clone(s.KeptAt)
|
||||||
for _, pm := range p.Modules {
|
for _, pm := range p.Modules {
|
||||||
if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 &&
|
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 {
|
if keptAt == nil {
|
||||||
keptAt = map[string]string{}
|
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 {
|
for _, pm := range p.Modules {
|
||||||
m, ok := modules[pm.Module]
|
m, ok := modules[pm.Module]
|
||||||
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
|
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: no build of the new form is what
|
// A module it runs inside its own composition is composed there as before while it is folded: no build of the
|
||||||
// it runs (Scope.InOwn).
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
if _, refused := in.Refused[pm.Module]; refused {
|
if _, refused := in.Refused[pm.Module]; refused {
|
||||||
@@ -1230,3 +1232,38 @@ func withoutHeldData(in compose.Input, node string) compose.Input {
|
|||||||
}
|
}
|
||||||
return out
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -204,6 +206,83 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
|||||||
if fetched-archives != 1 {
|
if fetched-archives != 1 {
|
||||||
t.Errorf("restic's archive was read %d times over two compositions, want once", fetched-archives)
|
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.
|
// 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
|
// 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.
|
// after the resource it runs after (issue 1126). Nil composes every folded module from its manifest, as before.
|
||||||
RunsOwn *delivery.Composed
|
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
|
// 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
|
// 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
|
// 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.
|
// not sent the field they would refuse.
|
||||||
r.Hands = takesHanded(node)
|
r.Hands = takesHanded(node)
|
||||||
r.Provisioner = provisionerAccount(plan)
|
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.Assigned = node.Handed
|
||||||
r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract
|
r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract
|
||||||
r.JudgesRoot = node.Stated && node.Contract >= RootContract
|
r.JudgesRoot = node.Stated && node.Contract >= RootContract
|
||||||
@@ -190,6 +196,21 @@ func provisionerAccount(plan catalogue.Resolution) string {
|
|||||||
return ""
|
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
|
// runsTheProvisionerClient says a module runs its machine's provisioner client: it claims the seat and declares the own
|
||||||
// secret that account is delivered in.
|
// secret that account is delivered in.
|
||||||
func runsTheProvisionerClient(m catalogue.Manifest) bool {
|
func runsTheProvisionerClient(m catalogue.Manifest) bool {
|
||||||
|
|||||||
@@ -218,6 +218,13 @@ type Pass struct {
|
|||||||
Earlier bool `json:"earlier,omitempty"`
|
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
|
// 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).
|
// that did not answer (laterAgain's first).
|
||||||
var askedWithin = 5 * time.Second
|
var askedWithin = 5 * time.Second
|
||||||
@@ -915,6 +922,13 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
|||||||
}
|
}
|
||||||
c.paid()
|
c.paid()
|
||||||
pass.Held = held
|
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
|
// 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).
|
// 1126: one module's refusal froze the whole node).
|
||||||
done := c.waiting("the agent marking each held module's have")
|
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
|
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