mesh/delivery merged: source.main moved to novox/mesh-sdk@be77f6656326
mesh/delivery-group group fix/1126-shadow-bodies merged: every member is merged
mesh/check pass 362a3f1378aa on 03da552cb331 | unit 2860 run, 1141 kept, 36 skipped | integration 2887 run, 1241 kept, 29 skipped | over bound: unit (…
ace and g14 answered 'nothing in this mesh provides artifact-store … assign distribution to a node' while novox runs it: a module another node cannot run in this composition (catalogue Resolution.Unhostable) offers nothing, silently. The refusal now names each such module and what it lacks, and each node left out. And the culprit search composed the whole mesh once per placed module for a refusal no stub changes (ace's answer took 7m46s): only the modules the refusal names are tried, at most 8.
210 lines
12 KiB
Go
210 lines
12 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"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"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// Issue 1126 (the seed's shadow, round 2): a node's answer names every module it cannot compose, not the first only;
|
|
// and a module folded into its own composition that names a build of the old form (de-spiegel on novox, nats, photos,
|
|
// systemd-resolved: their haves name a build-... id and no fingerprint) is left to the node, carried as it runs.
|
|
func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
|
ctx := context.Background()
|
|
image := "alpine@sha256:" + strings.Repeat("a", 64)
|
|
manifest := func(module string) []byte {
|
|
// Its data names its directory, as nats's names ${dir:mesh-state} (issue 1126: novox's own composition refused
|
|
// "nats says ${dir:mesh-state}, and nats declares no directory mesh-state" when its stub held no directory).
|
|
return []byte(`{"module":"` + module + `","version":"1","capabilities":["container-runtime"],` +
|
|
`"own-secrets":{"token":"${dir:state}/token"},` +
|
|
`"resources":[{"id":"state","type":"directory","path":"/var/lib/` + module + `","mode":"0700"},` +
|
|
`{"id":"server","type":"container","name":"` + module + `","artifact":"site"}],` +
|
|
`"build":{"artifacts":[{"name":"site","kind":"upstream","from":"` + image + `"}]}}`)
|
|
}
|
|
lemurs := []byte(`{"module":"lemurs","version":"1","resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`)
|
|
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
|
|
mk := func(placed ...string) *bus {
|
|
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
|
for _, m := range placed {
|
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
|
}
|
|
b := &bus{placements: map[string]delivery.Placement{"n1": p},
|
|
records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}},
|
|
modules: map[string]delivery.Module{"lemurs": {Manifest: delivery.ManifestDigest(lemurs)}},
|
|
manifests: map[string][]byte{delivery.ManifestDigest(lemurs): lemurs},
|
|
builds: map[string]*delivery.BuilderBuild{}}
|
|
for _, m := range []string{"web", "api", "spiegel"} {
|
|
b.own = append(b.own, compose.OwnSecretRow{Node: "n1", Module: m, Name: "token", Origin: "made", SealedTo: "sk", Sealed: `"box"`})
|
|
}
|
|
for _, m := range []string{"web", "api", "spiegel"} {
|
|
raw := manifest(m)
|
|
b.manifests[delivery.ManifestDigest(raw)] = raw
|
|
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
|
}
|
|
// web and api are built, their keys done and waiting for the reference of their image; spiegel has no build of
|
|
// the new form at all.
|
|
for i, m := range []string{"web", "api"} {
|
|
mod := b.modules[m]
|
|
mod.FP2 = fp(string(rune('1' + i)))
|
|
b.modules[m] = mod
|
|
b.builds[m+" "+mod.FP2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
|
|
Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "image", Digest: "sha256:" + strings.Repeat("b", 64)}}}
|
|
}
|
|
return b
|
|
}
|
|
given := func(b *bus) map[string]*delivery.Placement {
|
|
p := b.placements["n1"]
|
|
return map[string]*delivery.Placement{"n1": &p}
|
|
}
|
|
|
|
// Every refusal: two builds taken in, and a module the composition refuses (its image never built), each named.
|
|
b := mk("lemurs", "web", "api", "spiegel")
|
|
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b)},
|
|
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "h1", 3)
|
|
var refused []string
|
|
for _, r := range a.Refusals {
|
|
refused = append(refused, r.Module)
|
|
}
|
|
if a.Verdict != delivery.CandidateRefused || a.Refused != "" || !reflect.DeepEqual(refused, []string{"api", "spiegel", "web"}) {
|
|
t.Errorf("every refusal: %+v, want api, spiegel and web refused, each named", a)
|
|
}
|
|
for _, r := range a.Refusals {
|
|
want := map[string]string{"api": "waits for the reference of site", "web": "waits for the reference of site",
|
|
"spiegel": "spiegel has not been built for this mesh"}[r.Module]
|
|
if !strings.Contains(r.Why, want) {
|
|
t.Errorf("%s refused as %q, want %q", r.Module, r.Why, want)
|
|
}
|
|
}
|
|
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil {
|
|
t.Errorf("the answer is not one a have holds: %v (%s)", err, raw)
|
|
}
|
|
if lemursOK := slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }); !lemursOK {
|
|
t.Errorf("lemurs, which composes, is not answered beside the refusals: %+v", a)
|
|
}
|
|
|
|
// The fold: spiegel runs inside the node's own composition at a build of the old form. Its resources are carried as
|
|
// they run, each after the one it runs after: the node's own is the one it runs, and the node composes itself.
|
|
b = mk("lemurs", overlay.Name, "spiegel")
|
|
p := b.placements["n1"]
|
|
p.Decided.Endpoint = "n1.example:51820"
|
|
b.placements["n1"] = p
|
|
fps := map[string]string{"lemurs": fp("e")}
|
|
stub := Scope{Node: "n1"}
|
|
in, err := Input(ctx, b, Mesh{}, stub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.Nodes[0].Kept = map[string]catalogue.Manifest{"spiegel": compose.Stub(in.Catalogue["spiegel"])}
|
|
base, _, err := compose.Compositions(in, "n1", fps, compose.Reading)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A refusal naming no module placed here is no module's: no compose is spent stubbing each in turn (issue 1126: ace's
|
|
// answer took 7m46s, one compose per module), though here a stub would compose.
|
|
if m := culprit(in, "n1", fps, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" {
|
|
t.Errorf("a refusal naming no placed module was laid on %s", m)
|
|
}
|
|
ownKey, _ := delivery.ComposedNodeKey("n1")
|
|
own := base[ownKey]
|
|
carriedRes := []json.RawMessage{
|
|
json.RawMessage(`{"id":"spiegel.state","type":"directory","path":"/var/lib/spiegel","mode":"0700"}`),
|
|
json.RawMessage(`{"id":"spiegel.server","type":"container","name":"spiegel","image":"registry.example/spiegel@sha256:` + strings.Repeat("c", 64) + `"}`)}
|
|
// They run after the node's first own resource.
|
|
own.Resources = append(append(append([]json.RawMessage{}, own.Resources[:1]...), carriedRes...), own.Resources[1:]...)
|
|
var running []delivery.Composed
|
|
for k, c := range base {
|
|
if k == ownKey {
|
|
c = own
|
|
}
|
|
running = append(running, c)
|
|
}
|
|
scope := Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running}
|
|
got, _, err := Compose(ctx, b, Mesh{}, scope, fps)
|
|
if err != nil {
|
|
t.Fatalf("the node does not compose itself with spiegel folded at a build of the old form: %v", err)
|
|
}
|
|
gotOwn, _ := json.Marshal(got[ownKey].Resources)
|
|
wantOwn, _ := json.Marshal(own.Resources)
|
|
if string(gotOwn) != string(wantOwn) {
|
|
t.Errorf("the node's own composition is\n%s\nwant what runs\n%s", gotOwn, wantOwn)
|
|
}
|
|
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, running, "h1", 3)
|
|
if a.Verdict != delivery.CandidateSame {
|
|
t.Errorf("the fold's answer: %+v, want the same", a)
|
|
}
|
|
// (A) The agent also says spiegel runs by module, from the declaration kept before the switch (agent.Everything): a
|
|
// module the composition folds is judged inside the node's own, not as removed (issue 1126, seed #531's answer).
|
|
keptToo := append(append([]delivery.Composed{}, running...), delivery.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes})
|
|
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "h1", 3); a.Verdict != delivery.CandidateSame || len(a.Removed) != 0 {
|
|
t.Errorf("a folded module running from the kept declaration is answered %+v, want the same", a)
|
|
}
|
|
// (B) The node's own composition carries its bus membership as it runs it (the controller's, from its store): composed
|
|
// the same, not 'in what runs, not composed' (seed #531, novox).
|
|
membership := json.RawMessage(`{"id":"bus-membership","type":"file","path":"` + catalogue.BusMembershipPath + `","sealed":"{\"box\":\"m\"}","mode":"0600"}`)
|
|
withMembership := make([]delivery.Composed, 0, len(running))
|
|
for _, c := range running {
|
|
if c.Module == "" {
|
|
c.Resources = append(append([]json.RawMessage{}, c.Resources...), membership)
|
|
}
|
|
withMembership = append(withMembership, c)
|
|
}
|
|
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "h1", 3); a.Verdict != delivery.CandidateSame {
|
|
t.Errorf("the node's own with its bus membership is answered %+v, want the same", a)
|
|
}
|
|
// Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it.
|
|
if _, _, err := Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, fps); err == nil ||
|
|
!strings.Contains(err.Error(), "spiegel has not been built for this mesh") {
|
|
t.Errorf("composed spiegel without what runs: %v", err)
|
|
}
|
|
}
|
|
|
|
// A module another node cannot run in this composition offers nothing in it: the refusal names it and why
|
|
// (issue 1126, probe #532: ace and g14 answered "nothing in this mesh provides artifact-store … assign distribution to a
|
|
// node" while novox runs it). And a refusal no module's stub changes is answered at once, not after a compose per module
|
|
// (ace took 7m46s).
|
|
func otherNodeNotResolvedIsNamed(t *testing.T) {
|
|
ctx := context.Background()
|
|
prov := []byte(`{"module":"prov","version":"1","capabilities":["container-runtime"],"provides":[{"name":"thing","scope":"mesh"}]}`)
|
|
cons := []byte(`{"module":"cons","version":"1","requires":["thing"]}`)
|
|
// n2 runs prov, which needs a container runtime n2's record does not say it has: n2's set does not resolve here.
|
|
b := &bus{placements: map[string]delivery.Placement{
|
|
"n1": {Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Endpoint: "n1.example:51820"}, Modules: []delivery.PlacedModule{{Module: "cons"}, {Module: overlay.Name}}},
|
|
"n2": {Node: "n2", Decided: delivery.NodeDecided{Address: "10.77.0.2"}, Modules: []delivery.PlacedModule{{Module: "prov"}, {Module: overlay.Name}}}},
|
|
records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t)}, "n2": {Node: "n2", TunnelKey: publicKey(t)}},
|
|
modules: map[string]delivery.Module{"prov": {Manifest: delivery.ManifestDigest(prov)}, "cons": {Manifest: delivery.ManifestDigest(cons)}},
|
|
manifests: map[string][]byte{delivery.ManifestDigest(prov): prov, delivery.ManifestDigest(cons): cons}}
|
|
given := map[string]*delivery.Placement{}
|
|
for n, p := range b.placements {
|
|
p := p
|
|
given[n] = &p
|
|
}
|
|
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given}, map[string]string{"cons": "src2:" + strings.Repeat("1", 64)}, nil, "h", 1)
|
|
if a.Verdict != delivery.CandidateRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") {
|
|
t.Errorf("a refusal for want of n2's provider = %+v", a)
|
|
}
|
|
}
|
|
|
|
// A node that reaches no artifact store says what its composition holds of one (issue 1126: ace and g14 answered
|
|
// "reaches no artifact store", and nothing said why).
|
|
func noStoreReachedSaysWhy(t *testing.T) {
|
|
in := compose.Input{Catalogue: map[string]catalogue.Manifest{
|
|
"distribution": {Module: "distribution", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5000}}}},
|
|
Nodes: []compose.Node{{Name: "g14", Assigned: []string{"adwaita"}}, {Name: "novox", Assigned: []string{"distribution"}}}}
|
|
if got := storeSought(in); got != "the composition holds g14, novox; artifact-store is served by distribution on novox, none of it on the private network with a port" {
|
|
t.Errorf("said %q", got)
|
|
}
|
|
in.Nodes = in.Nodes[:1]
|
|
if got := storeSought(in); got != "the composition holds g14; no module placed there serves artifact-store" {
|
|
t.Errorf("said %q", got)
|
|
}
|
|
}
|