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, "p-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, "p-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, "p-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, "p-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, "p-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, "p-h1", 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)
|
|
}
|
|
}
|