Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e32583089 | ||
|
|
6019c6d514 | ||
|
|
054754482c | ||
|
|
f7129fdb41 | ||
|
|
f7abd05981 | ||
|
|
514cf23aef | ||
|
|
95b509e0d2 | ||
|
|
83e135f101 | ||
|
|
30b86ea602 | ||
|
|
32a99144f6 | ||
|
|
3544d644ba | ||
|
|
a0ce37fb2c |
@@ -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 {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -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,92 @@ 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
|
||||||
|
// filledHeld holds every module with a filled file (restic, its build not done) and not the client: the node still
|
||||||
|
// names the account (issue 774, ace 2026-11-04: its build modules and ssh-client held, no filled file, no account)
|
||||||
|
filledHeld bool
|
||||||
|
}{{true, true, false, false}, {false, true, false, false}, {true, false, false, false}, {false, false, false, false},
|
||||||
|
{true, true, true, false}, {true, true, false, 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.filledHeld {
|
||||||
|
bb.Builds["restic "+fps["restic"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding}
|
||||||
|
} 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 && !row.filledHeld) {
|
||||||
|
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 _, resticHeld := held["restic"]; row.filledHeld && !resticHeld {
|
||||||
|
t.Errorf("%+v: restic held %v", row, resticHeld)
|
||||||
|
}
|
||||||
|
if row.placed && !row.userless && !row.filledHeld && !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
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// What an input says of its held modules on the wire (issue 774, the group check of feat/l3-final 278aa5f0ccbe). A held
|
||||||
|
// module stands in the input as its stub (Stub), and the stub's mark (catalogue.Manifest.Stubbed) is no field of a
|
||||||
|
// manifest's JSON: an input a node-engine sent the check's sandbox lost it, so the sandbox read node-tools, held by its
|
||||||
|
// manifest's verdict, as a tool runner with no bundle and refused every module of ace, g14 and shanks for "its build
|
||||||
|
// produced 0 bundle(s)" instead of naming the verdict. So the input carries the mark itself, by module, for its catalogue
|
||||||
|
// and for each node's kept manifests, and a reader marks each again. An input from a node-engine before the mark (no
|
||||||
|
// Held) is read by its contract: a module Refused names stands in Catalogue as its stub.
|
||||||
|
|
||||||
|
// heldMarks are the modules an input holds as their stubs: those of its catalogue, and those each node keeps, by node.
|
||||||
|
type heldMarks struct {
|
||||||
|
Catalogue []string `json:",omitempty"`
|
||||||
|
Kept map[string][]string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputFields is Input without its JSON methods.
|
||||||
|
type inputFields Input
|
||||||
|
|
||||||
|
// inputWire is an input as it crosses the bus: its fields, and the modules it holds as their stubs.
|
||||||
|
type inputWire struct {
|
||||||
|
inputFields
|
||||||
|
Held *heldMarks `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON writes the input with the modules it holds as their stubs named beside it (heldMarks).
|
||||||
|
func (in Input) MarshalJSON() ([]byte, error) {
|
||||||
|
w := inputWire{inputFields: inputFields(in)}
|
||||||
|
marks := heldMarks{Catalogue: stubbedOf(in.Catalogue)}
|
||||||
|
for _, n := range in.Nodes {
|
||||||
|
if kept := stubbedOf(n.Kept); len(kept) > 0 {
|
||||||
|
if marks.Kept == nil {
|
||||||
|
marks.Kept = map[string][]string{}
|
||||||
|
}
|
||||||
|
marks.Kept[n.Name] = kept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(marks.Catalogue) > 0 || len(marks.Kept) > 0 {
|
||||||
|
w.Held = &marks
|
||||||
|
}
|
||||||
|
return json.Marshal(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reads an input and marks again each module it holds as its stub: those it names held, and, by the
|
||||||
|
// input's contract, every module Refused names that its catalogue holds.
|
||||||
|
func (in *Input) UnmarshalJSON(raw []byte) error {
|
||||||
|
var w inputWire
|
||||||
|
if err := json.Unmarshal(raw, &w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*in = Input(w.inputFields)
|
||||||
|
held := map[string]bool{}
|
||||||
|
for m := range in.Refused {
|
||||||
|
held[m] = true
|
||||||
|
}
|
||||||
|
if w.Held != nil {
|
||||||
|
for _, m := range w.Held.Catalogue {
|
||||||
|
held[m] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for m := range held {
|
||||||
|
if man, ok := in.Catalogue[m]; ok {
|
||||||
|
man.Stubbed = true
|
||||||
|
in.Catalogue[m] = man
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.Held != nil {
|
||||||
|
for i := range in.Nodes {
|
||||||
|
for _, m := range w.Held.Kept[in.Nodes[i].Name] {
|
||||||
|
if man, ok := in.Nodes[i].Kept[m]; ok {
|
||||||
|
man.Stubbed = true
|
||||||
|
in.Nodes[i].Kept[m] = man
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubbedOf is each module of manifests held as its stub, sorted.
|
||||||
|
func stubbedOf(manifests map[string]catalogue.Manifest) []string {
|
||||||
|
var out []string
|
||||||
|
for name, m := range manifests {
|
||||||
|
if m.Stubbed {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -115,6 +115,11 @@ func (d Declared) Body() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
if len(d.Filled) > 0 {
|
if len(d.Filled) > 0 {
|
||||||
envelope["filled_files"] = d.Filled
|
envelope["filled_files"] = d.Filled
|
||||||
|
}
|
||||||
|
// The provisioner client's account is the node's, named whenever a client runs here, filled files or not: a node
|
||||||
|
// whose every filled-file module is held this pass names none of its files, and still keeps them, so the client's
|
||||||
|
// writes of them must be accepted (issue 774, ace 2026-11-04: its build modules and ssh-client held, no account).
|
||||||
|
if d.Provisioner != "" {
|
||||||
envelope["provisioner_account"] = d.Provisioner
|
envelope["provisioner_account"] = d.Provisioner
|
||||||
}
|
}
|
||||||
// An empty declaration is deliberate: the node owns nothing the mesh put there (novox/hq issue 127), and the host refuses an
|
// An empty declaration is deliberate: the node owns nothing the mesh put there (novox/hq issue 127), and the host refuses an
|
||||||
@@ -202,9 +207,7 @@ func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) {
|
|||||||
Filled: composed.Filled, LocalNetwork: resolution.LocalNetworkPorts(rules, with.LocalNetworks),
|
Filled: composed.Filled, LocalNetwork: resolution.LocalNetworkPorts(rules, with.LocalNetworks),
|
||||||
CoreBoxesWanted: rendered.CoreBoxesWanted, Said: rendered.Said,
|
CoreBoxesWanted: rendered.CoreBoxesWanted, Said: rendered.Said,
|
||||||
Sequence: node.Sequence, Epoch: node.Epoch, Generation: node.Generation}
|
Sequence: node.Sequence, Epoch: node.Epoch, Generation: node.Generation}
|
||||||
if len(composed.Filled) > 0 {
|
out.Provisioner = with.Provisioner
|
||||||
out.Provisioner = with.Provisioner
|
|
||||||
}
|
|
||||||
// A handed module only pulled in by another keeps what requires it, for its value of its own in the assignments mirror (issue 765).
|
// A handed module only pulled in by another keeps what requires it, for its value of its own in the assignments mirror (issue 765).
|
||||||
for module := range composed.Plans {
|
for module := range composed.Plans {
|
||||||
if by := resolution.RequiredBy(plan, module); by != "" {
|
if by := resolution.RequiredBy(plan, module); by != "" {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
|
|||||||
for name, m := range base.Catalogue {
|
for name, m := range base.Catalogue {
|
||||||
shelf[name] = m
|
shelf[name] = m
|
||||||
}
|
}
|
||||||
|
// A module the input's node-engine holds as refused (compose.Input.Refused: its verdict, or bytes that do not decode) is
|
||||||
|
// held here as there, and its read says why, whatever the case's own verdicts: the check then names the cause the node
|
||||||
|
// holds it for, never a pass the node would not compose, nor a rule about its stub (issue 774, group check 278aa5f0ccbe).
|
||||||
|
for name, why := range base.Refused {
|
||||||
|
if read, carried := cv.Manifests[name]; carried && read.Accepted {
|
||||||
|
cv.Manifests[name] = ManifestRead{Refused: "its node-engine holds it: " + why}
|
||||||
|
}
|
||||||
|
}
|
||||||
in.Catalogue = shelf
|
in.Catalogue = shelf
|
||||||
in.Nodes = slices.Clone(in.Nodes)
|
in.Nodes = slices.Clone(in.Nodes)
|
||||||
in.Holdings = slices.Clone(in.Holdings)
|
in.Holdings = slices.Clone(in.Holdings)
|
||||||
|
|||||||
@@ -179,6 +179,47 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
|
|||||||
t.Errorf("a module the node-engine holds too: %+v", m)
|
t.Errorf("a module the node-engine holds too: %+v", m)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's
|
||||||
|
// verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses
|
||||||
|
// it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way.
|
||||||
|
t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) {
|
||||||
|
raw := `{"module": "runner", "version": "1", "tool-runner": true, ` +
|
||||||
|
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
|
||||||
|
man, err := catalogue.Decode([]byte(raw))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"runner": compose.Stub(man)},
|
||||||
|
Refused: map[string]string{"runner": "the indexer's judge refused the manifest of runner"},
|
||||||
|
Nodes: []compose.Node{{Name: "a", Assigned: []string{"runner"}}}}
|
||||||
|
wire, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var back compose.Input
|
||||||
|
if err := json.Unmarshal(wire, &back); err != nil || !back.Catalogue["runner"].Stubbed || !strings.Contains(string(wire), `"Held"`) {
|
||||||
|
t.Fatalf("the input's held mark across the wire: %s, %v", wire, err)
|
||||||
|
}
|
||||||
|
older := strings.Replace(string(wire), `"Held":{"Catalogue":["runner"]}`, `"Unsaid":null`, 1)
|
||||||
|
for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} {
|
||||||
|
var r Request
|
||||||
|
r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
|
||||||
|
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}}}}}
|
||||||
|
if err := json.Unmarshal([]byte(sent), &r.Input); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
v, err := Run(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if m := v.Cases[0].Machines["a"]; !m.Composes || strings.Contains(m.Reason, "bundle") {
|
||||||
|
t.Errorf("%s: the node with its tool runner held: %+v", name, m)
|
||||||
|
}
|
||||||
|
if read := v.Cases[0].Manifests["runner"]; read.Accepted || !strings.Contains(read.Refused, "the indexer's judge refused the manifest of runner") {
|
||||||
|
t.Errorf("%s: the runner's read names not why its node-engine holds it: %+v", name, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
|
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
|
||||||
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
|
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
|
||||||
t.Fatalf("a request with no machines: %+v, %v", v, err)
|
t.Fatalf("a request with no machines: %+v, %v", v, err)
|
||||||
|
|||||||
@@ -270,7 +270,8 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wantRecord := delivery.NodeRecord{V: delivery.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk",
|
wantRecord := delivery.NodeRecord{V: delivery.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk",
|
||||||
Certificate: "cert", Trusts: keys.Trusts, Capabilities: []string{"apt", "systemd"}, OutwardLinks: []string{"eth0"}, NetworksSaid: true,
|
Certificate: "cert", Trusts: keys.Trusts, Capabilities: []string{"apt", "systemd"}, OutwardLinks: []string{"eth0"}, NetworksSaid: true,
|
||||||
Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, Contract: NodeKeyContract}
|
Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, Contract: NodeKeyContract,
|
||||||
|
ManifestReads: catalogue.ManifestReads()} // what it reads of a manifest: its contract's (ADR 0614 §4)
|
||||||
if !delivery.SameNodeRecord(*got.Record, wantRecord) {
|
if !delivery.SameNodeRecord(*got.Record, wantRecord) {
|
||||||
t.Errorf("record = %+v, want %+v", *got.Record, wantRecord)
|
t.Errorf("record = %+v, want %+v", *got.Record, wantRecord)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package link
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
@@ -27,6 +28,11 @@ type RecordKeys struct {
|
|||||||
Trusts []string
|
Trusts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manifestReads is what this engine reads of a manifest (novox/hq ADR 0614 §4): the reader capabilities of the mesh-sdk it
|
||||||
|
// is built with, stated in its record so the assigner moves a want only to an engine that reads its manifest. Computed
|
||||||
|
// once: it is the build's, and never changes while it runs.
|
||||||
|
var manifestReads = sync.OnceValue(catalogue.ManifestReads)
|
||||||
|
|
||||||
// recordOf is the node's record from one facts report and its keys; nil for a report that carries no profile (one that
|
// recordOf is the node's record from one facts report and its keys; nil for a report that carries no profile (one that
|
||||||
// says nothing of the machine, such as a rekey), so the record last written stands.
|
// says nothing of the machine, such as a rekey), so the record last written stands.
|
||||||
func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord {
|
func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord {
|
||||||
@@ -47,7 +53,8 @@ func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
rec := &delivery.NodeRecord{V: delivery.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey,
|
rec := &delivery.NodeRecord{V: delivery.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey,
|
||||||
ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts}
|
ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts,
|
||||||
|
ManifestReads: manifestReads()}
|
||||||
for _, c := range p.Capabilities {
|
for _, c := range p.Capabilities {
|
||||||
if c.Present && c.Name != "" {
|
if c.Present && c.Name != "" {
|
||||||
rec.Capabilities = append(rec.Capabilities, c.Name)
|
rec.Capabilities = append(rec.Capabilities, c.Name)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ func TestTheManifestConsumesTheBackupEvent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// ADR 0614 §4: the node-engine's own manifest uses only words every running engine reads (the baseline, what an engine
|
||||||
|
// stating none is read as), so its new build is never held for a word only it reads.
|
||||||
|
if needs, err := catalogue.ManifestNeeds(raw); err != nil || len(needs) == 0 {
|
||||||
|
t.Fatalf("the manifest's needs: %v, %v", needs, err)
|
||||||
|
} else if missing := catalogue.MissingManifestReads(needs, nil); len(missing) != 0 {
|
||||||
|
t.Errorf("the manifest needs %v, which an engine stating nothing does not read: its new build would be held", missing)
|
||||||
|
}
|
||||||
// 774 (the alert table): the state health, one key per node, the node's own health report, kept until
|
// 774 (the alert table): the state health, one key per node, the node's own health report, kept until
|
||||||
// written again (no TTL: a node that stops writing keeps its last word, which the watchman reads with its time).
|
// written again (no TTL: a node that stops writing keeps its last word, which the watchman reads with its time).
|
||||||
// ADR 0577 clause 1: and the state data, one key per module placed on the node that declares data items, kept until
|
// ADR 0577 clause 1: and the state data, one key per module placed on the node that declares data items, kept until
|
||||||
|
|||||||
Reference in New Issue
Block a user