mesh/delivery merged: source.main moved to novox/mesh-sdk@47eb1009921d
mesh/delivery-group group fix/1126-switch-safe merged: every member is merged
mesh/check pass 7359d187db8d on 47b1dcf08f85 | unit 2992 run, 1008 kept, 36 skipped | integration 3025 run, 1102 kept, 29 skipped | over bound: unit (…
159 lines
8.0 KiB
Go
159 lines
8.0 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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)
|
|
}
|
|
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)
|
|
}
|
|
// 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 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)
|
|
}
|
|
}
|