mesh/delivery merged: source.main moved to novox/mesh-sdk@6a71d29b3331
mesh/delivery-group group fix/kept-read-and-foreseen-account merged: every member is merged
mesh/check pass 4e9621edd664 on 14de8dadb4ce | unit 2719 run, 602 kept, 36 skipped | integration 2731 run, 645 kept, 29 skipped | over bound: unit, in…
The proposal on ace said only "mesh-self-check runs as its own account and declares no own secret broker": the words
of its bare stub, held for "has no bus credential", which no one saw. The second hidden first reason of the day, after
"0 bundle(s)".
Quick fix: log the held map in composeEach. Thorough fix: firstReason leads a refusal no culprit is found for with the
first reason of every held module it names ("<m> is held: <why>; its stand-in is then refused: <err>"), for Compose and
the node's Answer alike; the hold's own why already keeps its first words. Chose thorough.
154 lines
8.0 KiB
Go
154 lines
8.0 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given.
|
|
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
|
t.Helper()
|
|
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
|
b := &bus{records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
|
modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}}
|
|
fps := map[string]string{}
|
|
for i, raw := range manifests {
|
|
module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0]
|
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: module})
|
|
b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))}
|
|
b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw)
|
|
fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64)
|
|
}
|
|
b.placements = map[string]delivery.Placement{"n1": p}
|
|
return b, fps
|
|
}
|
|
|
|
// Issue 774, 2026-10-30 19:30: a node refused whole after holding a module says what it held and why, since the last
|
|
// refusal can be of the held module's stub (node-tools held, then "its build produced 0 bundle(s)", its own why unsaid).
|
|
func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) {
|
|
refused := `{"module":"power","version":"1","items":[{"collection":"contribution:node-bar:block","attributes":{"shows":"state"},"template":"files/a"}]}`
|
|
needs := `{"module":"lamp","version":"1","requires":["nothing-provides-this"]}`
|
|
b, fps := aNode(t, refused, needs)
|
|
_, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1"}, fps)
|
|
if err == nil || !strings.Contains(err.Error(), "nothing provides") || !strings.Contains(err.Error(), `held before: power: `) ||
|
|
!strings.Contains(err.Error(), `"shows" is none of them`) {
|
|
t.Errorf("the node refused whole with %v (held %v), want lamp's refusal and power held before, with its why", err, held)
|
|
}
|
|
}
|
|
|
|
// Issue 774: the culprit rule takes a module whose stub composes or fails otherwise; the runtime only where its stub
|
|
// composes, since its stub always fails otherwise.
|
|
func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) {
|
|
err := errors.New("lamp refuses")
|
|
for _, row := range []struct {
|
|
module string
|
|
again error
|
|
blamed bool
|
|
}{
|
|
{"lamp", nil, true},
|
|
{"lamp", errors.New("other"), true},
|
|
{"lamp", errors.New("lamp refuses"), false},
|
|
{catalogue.RuntimeModule, nil, true},
|
|
{catalogue.RuntimeModule, errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false},
|
|
} {
|
|
if got := blames(row.module, row.again, err); got != row.blamed {
|
|
t.Errorf("%s with %v: blamed %v, want %v", row.module, row.again, got, row.blamed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Issue 774, 2026-10-30 19:30: the trunk's node-tools moved to a build not yet done, and ace, g14 and shanks composed
|
|
// it from the indexer's manifest, which names no bundle: "its build produced 0 bundle(s)", every module refused. A
|
|
// module that runs is now held until its build lands, the runtime composed as what runs of it; and a module wanted at
|
|
// another build than the trunk's is composed from the build its want names.
|
|
func aRunningModuleWaitsForItsBuild(t *testing.T) {
|
|
dg := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
|
|
tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
|
|
`"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}`
|
|
lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}`
|
|
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}}}`
|
|
old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64)
|
|
running := []delivery.Composed{{Node: "n1", Module: catalogue.RuntimeModule, FP: old, Resources: []json.RawMessage{
|
|
json.RawMessage(`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{"MESH_TOOL_MODULES":""}}`),
|
|
json.RawMessage(`{"id":"node-tools.bundle-runtime","module":"node-tools","type":"archive","source":"artifact-store://node-tools/runtime/blobs/` +
|
|
dg("1") + `","digest":"` + dg("1") + `","path":"` + catalogue.BundlePath("node-tools", "runtime") + `"}`)}}}
|
|
for _, row := range []struct {
|
|
name string
|
|
want string // the build node-tools' want names
|
|
done bool // the trunk's build is done
|
|
held string // words node-tools' hold says; empty: composed
|
|
digest string // the runtime bundle's digest composed
|
|
}{
|
|
{name: "the trunk's build not done yet", want: next, held: "no done build yet"},
|
|
{name: "wanted at the build that runs, the trunk's not done", want: old, digest: dg("1")},
|
|
{name: "the trunk's build done", want: next, done: true, digest: dg("2")},
|
|
} {
|
|
b, fps := aNode(t, store, tools, lamp)
|
|
b.own = []compose.OwnSecretRow{{Node: "n1", Module: "node-tools", Name: "broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}
|
|
mod := b.modules["node-tools"]
|
|
mod.FP2 = next
|
|
b.modules["node-tools"] = mod
|
|
b.builds["node-tools "+old] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
|
|
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}}
|
|
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest}
|
|
if row.done {
|
|
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
|
|
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}}
|
|
}
|
|
fps["node-tools"] = row.want
|
|
out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1", Running: running}, fps)
|
|
if err != nil {
|
|
t.Errorf("%s: the node is refused: %v", row.name, err)
|
|
continue
|
|
}
|
|
if lampKey, _ := delivery.ComposedKey("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" {
|
|
t.Errorf("%s: lamp is not composed (held %v)", row.name, held)
|
|
}
|
|
if why := held["node-tools"]; (row.held == "") != (why == "") || !strings.Contains(why, row.held) {
|
|
t.Errorf("%s: node-tools held %q, want %q", row.name, why, row.held)
|
|
}
|
|
if row.digest != "" {
|
|
key, _ := delivery.ComposedKey("n1", "node-tools", row.want)
|
|
if c := out[key]; !strings.Contains(fmt.Sprint(rawStrings(c.Resources)), row.digest) {
|
|
t.Errorf("%s: node-tools is composed as %v, want its bundle %s", row.name, rawStrings(c.Resources), row.digest)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func rawStrings(rs []json.RawMessage) []string {
|
|
var out []string
|
|
for _, r := range rs {
|
|
out = append(out, string(r))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Issue 774: a refusal of a held module's stand-in leads with why the module was held, its first reason, as mesh-self-check's
|
|
// on ace should have; a refusal naming no held module is as it was.
|
|
func aHeldModulesFirstReasonLeads(t *testing.T) {
|
|
held := map[string]string{"mesh-self-check": "mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check"}
|
|
for _, row := range []struct {
|
|
err, want string
|
|
}{
|
|
{"mesh-self-check runs as its own account and declares no own secret broker",
|
|
"mesh-self-check is held: mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check; " +
|
|
"its stand-in is then refused: mesh-self-check runs as its own account and declares no own secret broker"},
|
|
{"ace is the hub and has no endpoint", "ace is the hub and has no endpoint"},
|
|
} {
|
|
err := errors.New(row.err)
|
|
got := firstReason(err, held)
|
|
if got.Error() != row.want || !errors.Is(got, err) {
|
|
t.Errorf("refused %q, want %q", got, row.want)
|
|
}
|
|
}
|
|
}
|