mesh/delivery merged: source.main moved to novox/mesh-sdk@98f2f2406921
mesh/delivery-group group fix/1126-apply-culprit merged: every member is merged
mesh/check pass 0de30064156a on 7aedd28f01dc | unit 2667 run, 590 kept, 36 skipped | integration 2676 run, 633 kept, 29 skipped | over bound: unit (sl…
The applying path (frombus.Compose) composed with no culprit, so one module's refusal came back as the whole node's: novox's apply would have refused all its modules for de-spiegel's unset route names. composeEach is now the one rule Compose and Answer both follow: - a module whose stub changes the refusal is held (compose.Node.Held); in the applying path a stand-in for a module placed here holds that module; only a refusal of no single module refuses the node; - a held module is composed from what runs of it (compose.Carried of its own composition in Running) and carried in the node's own wherever that runs it, folded or leaving the fold, so the node's own apply never removes it (issue 1945 by a new path) and what the others read of it is what runs; - none of a held module's own composition is in what is applied; - a module whose composition differs when the held ones are composed as stubs is held too; the node's own differing refuses the node; - since a held module is composed from its new manifest, what reads that manifest is held with it: the providers of what it contributes to (the proxy its routes) and, when it listens, the filter's holder. Quick fix weighed: a culprit loop in Compose alone. Thorough, chosen: one shared function, since the kind met twice (seed-8 shadow, now the apply), and review rounds 1 and 2 found its leaks.
492 lines
28 KiB
Go
492 lines
28 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"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, compose.Foreseeing, 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, _, held, err := Compose(ctx, b, Mesh{}, scope, fps)
|
|
if err == nil && len(held) > 0 {
|
|
err = fmt.Errorf("modules held: %v", held)
|
|
}
|
|
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 _, _, held, err := Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, fps); err != nil ||
|
|
!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)
|
|
}
|
|
|
|
// (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
|
|
// stub had no container, so snake's 80 on ace was opened beside the route proxy's). And C6: the operator export's file
|
|
// a module that keeps it runs is carried as it runs (mesh-vault.kept on novox), since the node holds no export.
|
|
listening := []byte(`{"module":"spiegel","version":"1","capabilities":["container-runtime"],` +
|
|
`"listens":[{"port":8080,"from":"mesh","why":"the site"}],"own-secrets":{"token":"${dir:state}/token"},` +
|
|
`"resources":[{"id":"state","type":"directory","path":"/var/lib/spiegel","mode":"0700"},` +
|
|
`{"id":"server","type":"container","name":"spiegel","artifact":"site","ports":["8080"]}],` +
|
|
`"build":{"artifacts":[{"name":"site","kind":"upstream","from":"` + image + `"}]}}`)
|
|
nft := []byte(`{"module":"nft","version":"1","capabilities":["systemd"],"filtering":{"into":"/etc/nftables.conf"}}`)
|
|
vault := []byte(`{"module":"vault","version":"1","capabilities":["systemd"],"keeps":"/var/lib/vault/root",` +
|
|
`"resources":[{"id":"service","type":"service","unit":"vault.service","state":"running","boot":"enabled"}]}`)
|
|
b = mk("lemurs", overlay.Name, "spiegel", "nft", "vault")
|
|
for m, raw := range map[string][]byte{"spiegel": listening, "nft": nft, "vault": vault} {
|
|
b.manifests[delivery.ManifestDigest(raw)] = raw
|
|
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
|
}
|
|
p = b.placements["n1"]
|
|
p.Decided.Endpoint = "n1.example:51820"
|
|
for i := range p.Modules {
|
|
if p.Modules[i].Module == "spiegel" {
|
|
p.Modules[i].Ports = map[string]int{"8080": 20005}
|
|
}
|
|
}
|
|
b.placements["n1"] = p
|
|
rec := b.records["n1"]
|
|
rec.OutwardLinks = []string{"eth0"}
|
|
b.records["n1"] = rec
|
|
fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "vault": fp("9")}
|
|
if in, err = Input(ctx, b, Mesh{}, stub); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.Nodes[0].Kept = map[string]catalogue.Manifest{"spiegel": compose.Stub(in.Catalogue["spiegel"])}
|
|
if base, _, err = compose.Compositions(in, "n1", fps, compose.Reading); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
keptFile := json.RawMessage(`{"digest":"sha256:` + strings.Repeat("d", 64) + `","id":"vault.kept","mode":"0600","module":"vault","path":"/var/lib/vault/root/export.json","size":143604,"type":"file"}`)
|
|
running = nil
|
|
for _, c := range base {
|
|
switch c.Module {
|
|
case "":
|
|
c.Resources = append(append([]json.RawMessage{}, c.Resources...),
|
|
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)+`","ports":["10.77.0.1:20005:8080"]}`),
|
|
json.RawMessage(`{"id":"spiegel.server-env","type":"file","path":"/var/lib/spiegel/server.env","mode":"0600","content":"A=1\n"}`))
|
|
case "vault":
|
|
c.Resources = append(append([]json.RawMessage{}, c.Resources...), keptFile)
|
|
}
|
|
running = append(running, c)
|
|
}
|
|
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running}, fps)
|
|
if err == nil && len(held) > 0 {
|
|
err = fmt.Errorf("modules held: %v", held)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("the node does not compose itself with spiegel carried: %v", err)
|
|
}
|
|
var rules string
|
|
var keptComposed json.RawMessage
|
|
for _, c := range got {
|
|
for _, raw := range c.Resources {
|
|
var r struct {
|
|
ID string `json:"id"`
|
|
Content string `json:"content"`
|
|
}
|
|
_ = json.Unmarshal(raw, &r)
|
|
switch r.ID {
|
|
case "nft." + catalogue.FilteringID():
|
|
rules = r.Content
|
|
case "vault." + catalogue.KeptID():
|
|
keptComposed = raw
|
|
}
|
|
}
|
|
}
|
|
if !strings.Contains(rules, "# spiegel — the site") || !strings.Contains(rules, "dport 20005 accept") || strings.Contains(rules, "dport 8080") {
|
|
t.Errorf("the filter does not open the machine port spiegel runs at:\n%s", rules)
|
|
}
|
|
// The paths a carried module owns are its manifest's, as the controller composes them, never what runs of it (issue
|
|
// 1126, seed-8 #542: on ace, the folded f1-circuits, snake and txt-game, whose manifests leave their directories and
|
|
// files to ${dir:…}, owned their /var/lib paths on the node and none on the controller, shifting node-tools'
|
|
// MESH_OWNED_PATHS). f1-circuits' manifest at the indexer's commit (c0ea2c94) is captured; what runs of it is
|
|
// composed from that manifest by the controller's rule (the directory at /var/lib/f1-circuits, its file, its container).
|
|
for _, c := range running {
|
|
if c.Module == "" && !reflect.DeepEqual(compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).OwnsPaths, []string{"/var/lib/spiegel"}) {
|
|
t.Errorf("spiegel carried owns %v, want its manifest's /var/lib/spiegel alone", compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).OwnsPaths)
|
|
}
|
|
}
|
|
f1raw, err := os.ReadFile("testdata/f1-circuits-c0ea2c94.module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f1, err := catalogue.ParseManifest(f1raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f1runs := delivery.Composed{Node: "ace", Resources: []json.RawMessage{
|
|
json.RawMessage(`{"id":"f1-circuits.state","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits","mode":"0700"}`),
|
|
json.RawMessage(`{"id":"f1-circuits.cache","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits/cache","mode":"0700"}`),
|
|
json.RawMessage(`{"id":"f1-circuits.site-env","module":"f1-circuits","type":"file","path":"/var/lib/f1-circuits/site.env","mode":"0644","content":"F1_CIRCUITS_BASE_URL=https://f1.example\n"}`),
|
|
json.RawMessage(`{"id":"f1-circuits.server","module":"f1-circuits","type":"container","name":"f1-circuits","image":"registry.example/f1@sha256:` + strings.Repeat("c", 64) + `"}`)}}
|
|
if got, want := catalogue.DeclaredPaths(compose.Carried(f1, "f1-circuits", &f1runs)), catalogue.DeclaredPaths(f1); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("f1-circuits carried owns %v, want what the controller composes from its manifest, %v", got, want)
|
|
}
|
|
var k1, k2 any
|
|
_ = json.Unmarshal(keptComposed, &k1)
|
|
_ = json.Unmarshal(keptFile, &k2)
|
|
if !reflect.DeepEqual(k1, k2) {
|
|
t.Errorf("the operator export is composed as %s, want the file that runs %s", keptComposed, keptFile)
|
|
}
|
|
|
|
// (D) Issue 1126, seed-8 (mesh-catalog #542): a module's own refusal refuses that module, never the node. novox runs
|
|
// de-spiegel folded, and its manifest at the indexer's commit (e25e1beb, captured) asks four required config values
|
|
// for its route names that nothing sets. novox answered the whole candidate refused ("de-spiegel contributing
|
|
// praktijk to route says ${setting:praktijk-name} ...": its stub still contributed the route). Only de-spiegel is
|
|
// refused, in the config form's words, and proxy, which receives its routes (what it would compose lacks them: review
|
|
// of the apply, round 1); the rest is judged.
|
|
spiegel, err := os.ReadFile("testdata/de-spiegel-e25e1beb.module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proxy := []byte(`{"module":"proxy","version":"1","capabilities":["systemd"],"provides":[{"name":"route","scope":"mesh"},{"name":"smtp","scope":"mesh"}],` +
|
|
`"resources":[{"id":"service","type":"service","unit":"proxy.service","state":"running","boot":"enabled"}]}`)
|
|
b = mk("lemurs", overlay.Name, "proxy", "de-spiegel")
|
|
for m, raw := range map[string][]byte{"de-spiegel": spiegel, "proxy": proxy} {
|
|
b.manifests[delivery.ManifestDigest(raw)] = raw
|
|
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
|
}
|
|
p = b.placements["n1"]
|
|
p.Decided.Endpoint = "n1.example:51820"
|
|
b.placements["n1"] = p
|
|
fps = map[string]string{"lemurs": fp("e"), "proxy": fp("f")}
|
|
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"de-spiegel"}}, fps, nil, "h1", 3)
|
|
refused = nil
|
|
for _, r := range a.Refusals {
|
|
refused = append(refused, r.Module)
|
|
if r.Module == "de-spiegel" && (!strings.Contains(r.Why, "${config:praktijk-name}") || strings.Contains(r.Why, "${setting:")) {
|
|
t.Errorf("de-spiegel is refused in other words than its config form's: %s", r.Why)
|
|
}
|
|
}
|
|
if a.Refused != "" || !reflect.DeepEqual(refused, []string{"de-spiegel", "proxy"}) {
|
|
t.Errorf("a module's own refusal: %+v, want de-spiegel refused with proxy, which receives its routes, the node not", a)
|
|
}
|
|
if !slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }) {
|
|
t.Errorf("lemurs, which composes, is not judged beside de-spiegel's refusal: %+v", a)
|
|
}
|
|
|
|
// (E) Issue 1126, the applying path (review of the seed): the composition novox applies follows the same rule. It
|
|
// composed with no culprit, so de-spiegel's unset route names refused the whole node and every module it runs was
|
|
// marked not composed. Now de-spiegel is held, its refusal said in its config form's words, with proxy, which
|
|
// receives its routes; lemurs composes, and the node's own composition carries de-spiegel as it runs, so applying it
|
|
// removes nothing of it.
|
|
spiegelRuns := []json.RawMessage{
|
|
json.RawMessage(`{"id":"de-spiegel.state","type":"directory","path":"/var/lib/de-spiegel","mode":"0755"}`),
|
|
json.RawMessage(`{"id":"de-spiegel.server-env","type":"file","path":"/var/lib/de-spiegel/server.env","mode":"0600","content":"A=1\n"}`),
|
|
json.RawMessage(`{"id":"de-spiegel.server","type":"container","name":"de-spiegel","image":"registry.example/de-spiegel@sha256:` + strings.Repeat("c", 64) + `"}`)}
|
|
if in, err = Input(ctx, b, Mesh{}, stub); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.Nodes[0].Kept = map[string]catalogue.Manifest{"de-spiegel": compose.Stub(in.Catalogue["de-spiegel"])}
|
|
in.Nodes[0].Kept["de-spiegel"] = func(m catalogue.Manifest) catalogue.Manifest {
|
|
m.Contributes, m.ContributesMany = nil, nil
|
|
return m
|
|
}(in.Nodes[0].Kept["de-spiegel"])
|
|
if base, _, err = compose.Compositions(in, "n1", fps, compose.Reading); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
running = nil
|
|
for _, c := range base {
|
|
if c.Module == "" {
|
|
c.Resources = append(append([]json.RawMessage{}, c.Resources...), spiegelRuns...)
|
|
}
|
|
running = append(running, c)
|
|
}
|
|
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"de-spiegel"}, Running: running}, fps)
|
|
if err != nil {
|
|
t.Fatalf("de-spiegel's own refusal refuses the node's whole composition: %v", err)
|
|
}
|
|
if len(held) != 2 || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") || !strings.Contains(held["proxy"], "de-spiegel") {
|
|
t.Errorf("held %v, want de-spiegel in its config form's words, and proxy for its routes", held)
|
|
}
|
|
lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fps["lemurs"])
|
|
proxyKey, _ := delivery.ComposedKey("n1", "proxy", fps["proxy"])
|
|
if _, ok := got[lemursKey]; !ok {
|
|
t.Errorf("lemurs, which composes, is not composed beside de-spiegel's refusal: %v", sortedKeys(got))
|
|
}
|
|
if _, ok := got[proxyKey]; ok {
|
|
t.Errorf("proxy is composed without the routes of held de-spiegel: %v", sortedKeys(got))
|
|
}
|
|
carries := func(c delivery.Composed, what string) {
|
|
t.Helper()
|
|
ownRes, _ := json.Marshal(c.Resources)
|
|
for _, raw := range spiegelRuns {
|
|
var r struct{ ID string }
|
|
_ = json.Unmarshal(raw, &r)
|
|
if !strings.Contains(string(ownRes), `"id":"`+r.ID+`"`) {
|
|
t.Errorf("%s: the node's own composition drops %s, which de-spiegel runs: %s", what, r.ID, ownRes)
|
|
}
|
|
}
|
|
}
|
|
carries(got[ownKey], "folded")
|
|
// (F) Leaving the fold (review of the apply, round 1, finding 1): de-spiegel's want names a build of the new form,
|
|
// so it is no longer folded, while its names are still unset and the node's own still runs it. Held, it is carried
|
|
// in the node's own as it runs until a composition of its own is handed: else the node's own apply removes its
|
|
// container, files and units (issue 1945 by a new path).
|
|
fps["de-spiegel"] = fp("7")
|
|
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: running}, fps)
|
|
if err != nil || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") {
|
|
t.Fatalf("de-spiegel leaving the fold: %v, held %v", err, held)
|
|
}
|
|
spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", fps["de-spiegel"])
|
|
if _, ok := got[spiegelKey]; ok {
|
|
t.Errorf("held de-spiegel's own composition is handed")
|
|
}
|
|
carries(got[ownKey], "leaving the fold")
|
|
|
|
// (G) A held module that runs a composition of its own (review of the apply, round 1, finding 2): what the others
|
|
// compose of it is what runs, never its stub's. site listens on 8080, placed at the machine's 20005, and its own
|
|
// secret was never issued here: held, the filter must not open 8080 and drop the 20005 that runs. A module whose
|
|
// composition depends on how site is held (here the filter) is held with it.
|
|
site := []byte(`{"module":"site","version":"1","capabilities":["container-runtime"],` +
|
|
`"listens":[{"port":8080,"from":"mesh","why":"the site"}],"own-secrets":{"key":"${dir:state}/key"},` +
|
|
`"resources":[{"id":"state","type":"directory","path":"/var/lib/site","mode":"0700"},` +
|
|
`{"id":"server","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}]}`)
|
|
b = mk("lemurs", overlay.Name, "site", "nft")
|
|
for m, raw := range map[string][]byte{"site": site, "nft": nft} {
|
|
b.manifests[delivery.ManifestDigest(raw)] = raw
|
|
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
|
}
|
|
p = b.placements["n1"]
|
|
p.Decided.Endpoint = "n1.example:51820"
|
|
for i := range p.Modules {
|
|
if p.Modules[i].Module == "site" {
|
|
p.Modules[i].Ports = map[string]int{"8080": 20005}
|
|
}
|
|
}
|
|
b.placements["n1"] = p
|
|
rec = b.records["n1"]
|
|
rec.OutwardLinks = []string{"eth0"}
|
|
b.records["n1"] = rec
|
|
fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "site": fp("8")}
|
|
siteRuns := delivery.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{
|
|
json.RawMessage(`{"id":"site.state","module":"site","type":"directory","path":"/var/lib/site","mode":"0700"}`),
|
|
json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["10.77.0.1:20005:8080"]}`)}}
|
|
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps)
|
|
if err != nil || held["site"] == "" || held["nft"] == "" {
|
|
t.Fatalf("site, its own secret never issued: %v, held %v; want site held, and the filter, which depends on it", err, held)
|
|
}
|
|
for key, c := range got {
|
|
for _, raw := range c.Resources {
|
|
if strings.Contains(string(raw), "dport 8080") || (c.Module == "nft" && !strings.Contains(string(raw), "dport 20005") && strings.Contains(string(raw), "# site")) {
|
|
t.Errorf("%s composes held site from its stub, not from what runs: %s", key, raw)
|
|
}
|
|
}
|
|
}
|
|
// (H) Review of the apply, round 2: a held module is composed from its new manifest, so what the others read of its
|
|
// manifest (its listens, what it contributes) may not be what runs. site's new manifest moves it to 9090 while the
|
|
// build that runs listens on 8080: the filter, the holder of what site listens on, is held with it, and nothing
|
|
// applied opens 9090.
|
|
moved := []byte(strings.Replace(string(site), `"port":8080`, `"port":9090`, 1))
|
|
b.manifests[delivery.ManifestDigest(moved)] = moved
|
|
b.modules["site"] = delivery.Module{Manifest: delivery.ManifestDigest(moved)}
|
|
for i := range p.Modules {
|
|
p.Modules[i].Ports = nil
|
|
}
|
|
b.placements["n1"] = p
|
|
siteRuns.Resources[1] = json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}`)
|
|
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps)
|
|
if err != nil || held["site"] == "" || held["nft"] == "" {
|
|
t.Errorf("site moved to 9090 in its new manifest: %v, held %v; want site held, and the filter with it", err, held)
|
|
}
|
|
for key, c := range got {
|
|
for _, raw := range c.Resources {
|
|
if strings.Contains(string(raw), "dport 9090") {
|
|
t.Errorf("%s opens 9090, which held site's new manifest names and nothing runs: %s", key, raw)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|