package frombus import ( "archive/tar" "bytes" "compress/gzip" "context" "crypto/ecdh" "crypto/rand" "encoding/base64" "encoding/json" "errors" "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" ) // bus is a Reader over values held in memory, counting whose config values were read. type bus struct { placements map[string]delivery.Placement records map[string]delivery.NodeRecord modules map[string]delivery.Module manifests map[string][]byte values map[string]map[string]any // by node.module valuesRead []string own []compose.OwnSecretRow ownErr error // asked is every consumer credential asked; the fake refuses the whole ask, as the vault's own does, for a consuming // module its placement does not put on the node. asked []compose.CredentialKey // builds are the builder's keys, by " "; archives the artifact store's blobs, by digest, and fetched // every URL asked; served another node's values by ".", and servedRead every ask with its keys. builds map[string]*delivery.BuilderBuild archives map[string][]byte fetched []string served map[string]map[string]any servedRead []string buildsRead []string } func (b *bus) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) { b.buildsRead = append(b.buildsRead, module) return b.builds[module+" "+fp], nil } func (b *bus) Archive(_ context.Context, url, digest string) ([]byte, error) { b.fetched = append(b.fetched, url) body, ok := b.archives[digest] if !ok { return nil, errors.New("the store holds no such blob") } return body, nil } func (b *bus) Served(_ context.Context, node, module string, keys []string) (map[string]any, error) { b.servedRead = append(b.servedRead, node+"."+module+":"+strings.Join(keys, ",")) out := map[string]any{} for _, k := range keys { if v, ok := b.served[node+"."+module][k]; ok { out[k] = v } } return out, nil } func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) { return b.placements, nil } func (b *bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.records, nil } func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil } func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) { raw, ok := b.manifests[digest] if !ok { return nil, errors.New("no such manifest") } return raw, nil } func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) { return b.own, b.ownErr } func (b *bus) Credentials(_ context.Context, node string, asConsumer, _ []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) { b.asked = append(b.asked, asConsumer...) for _, k := range asConsumer { placed := false for _, m := range b.placements[node].Modules { placed = placed || m.Module == k.ConsumerModule } if !placed { return nil, nil, fmt.Errorf("%s is not on %s: neither assigned there nor required by a module there", k.ConsumerModule, node) } } return nil, nil, nil } func (b *bus) Values(_ context.Context, node, module string) (map[string]any, error) { b.valuesRead = append(b.valuesRead, node+"."+module) return b.values[node+"."+module], nil } func publicKey(t *testing.T) string { t.Helper() k, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes()) } // Issue 1126 (S3, S4, S5): the bus's placement, node records, manifests and config values fill the same compose.Input the // controller's store fills for compose's own two-machine mesh (compose's aMesh: anchor the hub, laptop), and that Input // composes; a node reads only its own config values; what cannot be composed is refused, naming it. func TestInput(t *testing.T) { // Issue 1126, the seed's shadow: every refusal named, an old-form module left to the node, and no store reached // saying why (answer_rows_test.go). t.Run("every refusal, and an old build left to the node", answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode) t.Run("no store reached says why", noStoreReachedSaysWhy) ctx := context.Background() xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") if err != nil { t.Fatal(err) } digest := delivery.ManifestDigest(xterm) // A module in the settings form (plex and the media modules on one node, 2026-10-26): its config values are in its // layers, not mesh-config, so the placement carries them (MeshValues, Values) and mesh-config's are not read for it. media := []byte(`{"module":"media","version":"1","settings":{"lines":{"kind":"preference","default":0,"why":"kept"},` + `"name":{"kind":"preference","default":"x","why":"shown"}},"own-secrets":{"token":"/srv/media/token"},"resources":[{"id":"conf","type":"file",` + `"path":"/srv/media/conf","mode":"0644","content":"uid=${setting:lines} name=${setting:name}\n"}]}`) mediaDigest := delivery.ManifestDigest(media) keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)} caps := []string{"container-runtime", "systemd", "wireguard"} tunnel := &delivery.RecordTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24", Peers: []delivery.RecordTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}} mk := func() *bus { return &bus{ placements: map[string]delivery.Placement{ "anchor": {Node: "anchor", Decided: delivery.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1", Tunnel: tunnel, PeerNames: map[string]string{"peer": "phone"}}, Modules: []delivery.PlacedModule{{Module: overlay.Name}}, Holds: []delivery.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}}, "laptop": {Node: "laptop", Decided: delivery.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"}, Modules: []delivery.PlacedModule{{Module: overlay.Name}, {Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}}, {Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true, Layer: map[string]any{"places": map[string]any{"data": "/srv/data"}}, Values: map[string]any{"lines": float64(1000)}}}}, }, records: map[string]delivery.NodeRecord{ "anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2}, "laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"}, }, modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "media": {Manifest: mediaDigest}, "unread": {}}, manifests: map[string][]byte{digest: xterm, mediaDigest: media}, values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"}, "laptop.media": {"lines": "not mesh-config's"}}, own: []compose.OwnSecretRow{{Node: "laptop", Module: "media", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}}, } } // What the controller's store gives for the same mesh, as compose's aMesh and SettingsFor build it. var private catalogue.Manifest raw, _ := json.Marshal(overlay.Manifest()) if err := json.Unmarshal(raw, &private); err != nil { t.Fatal(err) } parsed, err := catalogue.ParseManifestForInputs(xterm) if err != nil { t.Fatal(err) } parsedMedia, err := catalogue.ParseManifestForInputs(media) if err != nil { t.Fatal(err) } capsSet := map[string]bool{"container-runtime": true, "systemd": true, "wireguard": true} tun := compose.Tunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24", Peers: []compose.TunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}} want := compose.Input{ Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed, "media": parsedMedia}, Nodes: []compose.Node{ {Name: "anchor", Place: compose.Place{Key: keys["anchor"], Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1"}, Assigned: []string{overlay.Name}, Capabilities: capsSet, Contract: 2, Stated: true}, {Name: "laptop", Place: compose.Place{Key: keys["laptop"], Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"}, Assigned: []string{overlay.Name, "xterm", "media"}, Capabilities: capsSet, Ports: map[string]map[int]int{"xterm": {80: 8080}}, Settings: map[string][]catalogue.Layer{"xterm": {{From: "laptop", Values: map[string]any{"font": "mono"}}}, "media": {{From: catalogue.MeshWideLayer, Values: map[string]any{"name": "mesh"}}, {From: "laptop", Values: map[string]any{"lines": float64(1000)}, Placement: map[string]any{"places": map[string]any{"data": "/srv/data"}}}}}}, }, Holdings: []catalogue.Held{{Claim: "mesh-private-network", Scope: "mesh", Node: "anchor", Module: overlay.Name}}, OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: "media", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}}, // The same value as a core credential's box, for a core module (ADR 0349 §16). KeptCoreBoxes: []compose.CoreBox{{Node: "laptop", Module: "media", Name: "token", SealedTo: "sk", Box: json.RawMessage("{}")}}, Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}}, Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor", Adopted: true}, // The controller's order is by address; laptop enrolled under its peer's key, and a person named the other. Authority: "the mesh's certificate", CarriedPeers: []compose.CarriedPeer{{PublicKey: keys["laptop"], Address: "10.77.0.2", EnrolledAs: "laptop"}, {PublicKey: "peer", Address: "10.77.0.9", Named: "phone"}}, } b := mk() got, err := Input(ctx, b, Mesh{}, Scope{Node: "laptop"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { gj, _ := json.MarshalIndent(got, "", " ") wj, _ := json.MarshalIndent(want, "", " ") t.Fatalf("Input from the bus differs from the store's:\n got %s\nwant %s", gj, wj) } if !reflect.DeepEqual(b.valuesRead, []string{"laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) { t.Errorf("laptop read config values %v; a node reads its own slice alone", b.valuesRead) } // The same composer over both: the compositions from the bus are the store's, byte for byte. fps := map[string]string{overlay.Name: "src2:" + strings.Repeat("a", 64), "xterm": "src2:" + strings.Repeat("b", 64), "media": "src2:" + strings.Repeat("c", 64)} fromBus, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop"}, fps) if err != nil { t.Fatal(err) } fromStore, _, err := compose.Compositions(want, "laptop", fps, compose.Reading) if err != nil { t.Fatal(err) } if len(fromBus) == 0 || !reflect.DeepEqual(fromBus, fromStore) { t.Fatalf("compositions differ or are empty: %d from the bus, %d from the store", len(fromBus), len(fromStore)) } // Issue 1126: a candidate placement equal to the node's composes what it runs, and CompareComposed says so; one // thing changed in the group's candidate of another node (the hub's endpoint) names exactly the module it moves // here. Placed puts the same placement over the store's input unchanged. running := make([]delivery.Composed, 0, len(fromBus)) for _, c := range fromBus { running = append(running, c) } laptop := mk().placements["laptop"] asIs, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps) if err != nil { t.Fatal(err) } if c := delivery.CompareComposed(asIs, running); len(c.Differs)+len(c.Added)+len(c.Removed) != 0 || len(c.Same) != len(running) { t.Errorf("the candidate equal to the placement: %+v", c) } moved := mk().placements["anchor"] moved.Decided.Endpoint = "anchor.example:51821" changed, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps) if err != nil { t.Fatal(err) } if c := delivery.CompareComposed(changed, running); len(c.Differs) != 1 || c.Differs[0].Module != overlay.Name || c.Differs[0].Resource != overlay.Name+".overlay-config" { t.Errorf("the hub's endpoint changed: %+v, want %s alone, naming its config", c, overlay.Name) } if c := delivery.CompareComposed(changed, running[:0]); len(c.Added) != len(changed) { t.Errorf("nothing runs: %+v", c) } // ADR 0559 §4, the node's answer: the same; one module differing; a module the group adds, whose own secret is not // yet issued (no refusal); a value missing for a module the node runs (a refusal); a module the candidate removes. keeper := []byte(`{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` + `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}`) lemurs := []byte(`{"module":"lemurs","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`) // A provider and a consumer of a pair credential on the one node (compose's objects and files). objects, _ := json.Marshal(catalogue.Manifest{Module: "objects", Version: "1", Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh, Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}}, Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}}) files, _ := json.Marshal(catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"}, Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/files/config.json", "mode": "0600", "content": "{}", "merge": "json"}}}) one := 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}) } return &bus{placements: map[string]delivery.Placement{"n1": p}, records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}}, modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(lemurs)}, "objects": {Manifest: delivery.ManifestDigest(objects)}, "files": {Manifest: delivery.ManifestDigest(files)}}, manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs, delivery.ManifestDigest(objects): objects, delivery.ManifestDigest(files): files}, own: []compose.OwnSecretRow{{Node: "n1", Module: "keeper", Name: "password", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}} } oneFps := map[string]string{"keeper": "src2:" + strings.Repeat("d", 64), "lemurs": "src2:" + strings.Repeat("e", 64), "objects": "src2:" + strings.Repeat("f", 64), "files": "src2:" + strings.Repeat("0", 64)} runs, _, err := Compose(ctx, one("keeper", "lemurs"), Mesh{}, Scope{Node: "n1"}, oneFps) if err != nil { t.Fatal(err) } var runsNow []delivery.Composed for _, c := range runs { runsNow = append(runsNow, c) } objectsRun, _, err := Compose(ctx, one("objects"), Mesh{}, Scope{Node: "n1"}, oneFps) if err != nil { t.Fatal(err) } var consumerRuns []delivery.Composed for _, c := range objectsRun { consumerRuns = append(consumerRuns, c) } withoutOwn := func(cs []delivery.Composed) []delivery.Composed { var out []delivery.Composed for _, c := range cs { if c.Module != "" { out = append(out, c) } } return out } // What the controller runs on n1 for a module no want names (lemurs, at a build of the old form) and the private // network's (built by nobody): its composed writer (mesh-controller compositionsOf) folds their resources into the // node's own, each in its place, its module taken off; the rest split by module. tunnelled := func() *bus { b := one("keeper", "lemurs", overlay.Name) p := b.placements["n1"] p.Decided.Endpoint = "n1.example:51820" b.placements["n1"] = p return b } tunnelledAs := func() map[string]*delivery.Placement { p := tunnelled().placements["n1"] return map[string]*delivery.Placement{"n1": &p} } folded := func() []delivery.Composed { b := tunnelled() in, err := Input(ctx, b, Mesh{}, Scope{Node: "n1"}) if err != nil { t.Fatal(err) } reading := in reading.Nodes = append([]compose.Node(nil), in.Nodes...) reading.Nodes[0].ReadsModules = true d, _, err := compose.ComposeIn(reading, "n1", compose.Reading) if err != nil { t.Fatal(err) } body, err := d.Body() if err != nil { t.Fatal(err) } var envelope struct { Resources []map[string]json.RawMessage `json:"resources"` } if err := json.Unmarshal(body, &envelope); err != nil { t.Fatal(err) } var resources []json.RawMessage for _, r := range envelope.Resources { var module string _ = json.Unmarshal(r["module"], &module) if module == "lemurs" || module == overlay.Name { delete(r, "module") } raw, _ := json.Marshal(r) resources = append(resources, raw) } split, err := delivery.SplitComposed("n1", resources, map[string]string{"keeper": oneFps["keeper"]}, nil, delivery.NodeOwn{}) if err != nil { t.Fatal(err) } var out []delivery.Composed for _, c := range split { out = append(out, c) } return out }() if got := InOwn(folded, tunnelled().placements["n1"]); !reflect.DeepEqual(got, []string{"lemurs", overlay.Name}) { t.Errorf("InOwn of the controller's composition = %v, want lemurs and %s", got, overlay.Name) } placedAs := func(modules ...string) map[string]*delivery.Placement { p := one(modules...).placements["n1"] return map[string]*delivery.Placement{"n1": &p} } for name, c := range map[string]struct { b *bus given map[string]*delivery.Placement running []delivery.Composed verdict string check func(delivery.CandidateAnswer) bool }{ "the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateSame, func(a delivery.CandidateAnswer) bool { return len(a.Same) == 3 && a.Head == "h1" && a.Revision == 7 }}, "the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), delivery.CandidateSame, func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, "one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }}, "one added, its secret not yet issued": {func() *bus { b := one("lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"), nil, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool { for _, m := range a.Added { if m.Module == "keeper" { return reflect.DeepEqual(m.Unissued, []string{"keeper/password"}) && len(a.Refusals) == 0 } } return false }}, "a value missing for a module that runs": {func() *bus { b := one("keeper", "lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && strings.Contains(a.Refusals[0].Why, "keeper/password") }}, // Review of L2b, round 1, finding 1: a consumer the candidate adds has no credential yet, and the vault refuses an // ask for a module not on the node; it is not asked, and the module is added, its credential not yet issued. "a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool { return len(a.Added) == 1 && a.Added[0].Module == "files" && len(a.Added[0].Unissued) > 0 && len(a.Refusals) == 0 }}, // And with no trunk at all (the seed), a stand-in of a module the node runs is a refusal, not an addition. "the seed, a value missing for a module that runs": {func() *bus { b := one(); b.own = nil; return b }(), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && len(a.Added) == 0 }}, "cannot compose": {one("keeper"), map[string]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { return a.Refused != "" }}, // Issue 1126 (the seed's review): the private network's module and one at a build of the old form, which no want // names and the controller folds into the node's own, are folded there again: the node's own is the same, and // neither is added. "mesh-wireguard and an old-form module, folded": {tunnelled(), tunnelledAs(), folded, delivery.CandidateSame, func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, } { fps := oneFps if name == "mesh-wireguard and an old-form module, folded" { fps = map[string]string{"keeper": oneFps["keeper"]} // the node's wants: no build of lemurs or the private network } a := Answer(ctx, c.b, Mesh{}, Scope{Node: "n1", Placements: c.given, InOwn: InOwn(c.running, *c.given["n1"])}, fps, c.running, "h1", 7) if a.Verdict != c.verdict || !c.check(a) { t.Errorf("%s: %+v", name, a) } if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil { t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw) } } if got, err := Placed(want, map[string]*delivery.Placement{"laptop": &laptop}); err != nil || !reflect.DeepEqual(got, want) { t.Errorf("Placed with the node's own placement changes the input: %v", err) } if got, err := Placed(want, map[string]*delivery.Placement{"laptop": nil}); err != nil || len(got.Nodes[1].Assigned) != 0 || got.Nodes[1].Place.Key != keys["laptop"] { t.Errorf("Placed with no placement: %+v, %v", got.Nodes[1], err) } if _, err := Placed(want, map[string]*delivery.Placement{"stranger": {Node: "stranger"}}); err == nil { t.Error("Placed a node the input holds no node for") } if _, err := Input(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": {Node: "anchor"}}}); err == nil { t.Error("a given placement naming another node was taken") } // ADR 0555 §6: Export is Input's inverse for what a person decides: the store's input exported, node by node, is the // placement the bus holds, and read back it is the store's input again. for node, placed := range mk().placements { got, err := Export(want, node) if err != nil { t.Fatal(err) } if gj, _ := delivery.EncodePlacement(got); !delivery.SamePlacement(got, placed) { pj, _ := delivery.EncodePlacement(placed) t.Errorf("%s exported as %s, want %s", node, gj, pj) } } if _, err := Export(want, "stranger"); err == nil { t.Error("a node the input lacks was exported") } b = mk() if every, err := Input(ctx, b, Mesh{}, Scope{Every: true}); err != nil || every.OwnSecrets != nil { t.Fatalf("composing every node: %v, own secrets %v (the vault answers a node about itself alone)", err, every.OwnSecrets) } if !reflect.DeepEqual(b.valuesRead, []string{"anchor." + overlay.Name, "laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) { // Every node's placed modules, and nothing placed nowhere: anchor places no xterm. t.Errorf("the check read %v", b.valuesRead) } // A node none of whose modules declares an own secret composes without asking the vault (the review of L2: a node // with no secret composed nothing while the vault did not answer); one whose module does, refuses without it. quiet := mk() quiet.ownErr = errors.New("no responders") if _, err := Input(ctx, quiet, Mesh{}, Scope{Node: "anchor"}); err != nil { t.Errorf("anchor, with no own secret, needed the vault: %v", err) } if _, err := Input(ctx, quiet, Mesh{}, Scope{Node: "laptop"}); err == nil { t.Error("laptop's media, which declares an own secret, composed without the vault") } // A module the node's want keeps at another build than the trunk's composes from that build's manifest, resolved with // what it made (issue 1126), on the composing node alone. kept := mk() oldXterm := []byte(`{"module":"xterm","version":"1","resources":[{"id":"tools","type":"process","name":"xterm-tools",` + `"artifact":"tools","run":["./xterm-tools"]}],"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/xterm-tools","binary":"xterm-tools"}]}}`) kept.manifests["old"] = oldXterm oldFP := "src2:" + strings.Repeat("9", 64) toolsRef := "artifact-store://xterm/tools/blobs/sha256:" + strings.Repeat("1", 64) kept.builds = map[string]*delivery.BuilderBuild{"xterm " + oldFP: {State: delivery.BuildDone, Manifest: "old", Artifacts: []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: toolsRef, Digest: "sha256:" + strings.Repeat("1", 64)}}}} parsedOld, err := catalogue.ParseManifestForInputs(oldXterm) if err != nil { t.Fatal(err) } keptBuilt, err := parsedOld.Resolve([]catalogue.Built{{Name: "tools", Kind: "bundle", Reference: toolsRef, Digest: "sha256:" + strings.Repeat("1", 64)}}) if err != nil { t.Fatal(err) } in, err := Input(ctx, kept, Mesh{}, Scope{Node: "laptop", KeptAt: map[string]string{"xterm": oldFP}}) if err != nil || !reflect.DeepEqual(in.Nodes[1].Kept, map[string]catalogue.Manifest{"xterm": keptBuilt}) || in.Nodes[0].Kept != nil { t.Errorf("the want's build is kept on the composing node alone: %v, %v", in.Nodes, err) } // Issue 1126, the seed's refusals. restic and the provisioner's client as the catalogue holds them (mesh-catalog // c05aa5e6), restic moved with an own secret the vault leaves out (it hands it to the client, mesh-vault ownnode.go // handed): composed from the indexer's manifest restic is not handed and stands in for that secret, as novox's, // ace's, g14's and shanks's answers refused; composed from its build, it is handed, its templates archive read // where the node reaches the artifact store, and the composition is the one the controller's take-in gives. resticRaw, err := os.ReadFile("testdata/catalogue-c05aa5e6-restic.module.json") if err != nil { t.Fatal(err) } clientRaw, err := os.ReadFile("testdata/catalogue-c05aa5e6-mesh-provisioner-client.module.json") if err != nil { t.Fatal(err) } store := []byte(`{"module":"store","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],` + `"serves":{"artifact-store":{"port":5000}}}`) texts := map[string]string{} var packed bytes.Buffer zw := gzip.NewWriter(&packed) tw := tar.NewWriter(zw) for _, f := range []string{"backups.conf", "data.conf", "retention.conf"} { text, err := os.ReadFile("testdata/catalogue-c05aa5e6-restic.files/" + f) if err != nil { t.Fatal(err) } texts[f] = string(text) if err := tw.WriteHeader(&tar.Header{Name: "./" + f, Mode: 0o644, Size: int64(len(text)), Typeflag: tar.TypeReg}); err != nil { t.Fatal(err) } if _, err := tw.Write(text); err != nil { t.Fatal(err) } } if err := tw.Close(); err != nil { t.Fatal(err) } if err := zw.Close(); err != nil { t.Fatal(err) } archiveDigest := delivery.ManifestDigest(packed.Bytes()) digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) } movedFps := map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64), "store": "src2:" + strings.Repeat("5", 64)} resticArtifacts := []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: "artifact-store://restic/tools/blobs/" + digestOf("6"), Digest: digestOf("6")}, {Name: "files", Kind: "archive", Reference: "artifact-store://restic/files/blobs/" + archiveDigest, Digest: archiveDigest}} clientArtifacts := []delivery.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}} movedMesh := func(built bool) *bus { b := one() p := b.placements["n1"] p.Modules = []delivery.PlacedModule{{Module: "store"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}} p.Decided.Account, p.Decided.AccountHome = "operator", "/home/operator" b.placements["n1"] = p rec := b.records["n1"] rec.Contract, rec.Capabilities = compose.HandedContract, []string{"systemd", "package-manager"} b.records["n1"] = rec b.modules = map[string]delivery.Module{} b.manifests = map[string][]byte{} for module, raw := range map[string][]byte{"restic": resticRaw, "mesh-provisioner-client": clientRaw, "store": store} { b.modules[module] = delivery.Module{FP2: movedFps[module], Manifest: delivery.ManifestDigest(raw)} b.manifests[delivery.ManifestDigest(raw)] = raw } // The vault's own answer: the client's own secret; restic's is the client's to ask, so own leaves it out. b.own = []compose.OwnSecretRow{{Node: "n1", Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}} if built { b.builds = map[string]*delivery.BuilderBuild{ "restic " + movedFps["restic"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(resticRaw), Artifacts: resticArtifacts}, "mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(clientRaw), Artifacts: clientArtifacts}, "store " + movedFps["store"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(store)}, } b.archives = map[string][]byte{archiveDigest: packed.Bytes()} } return b } if _, _, err := Compose(ctx, movedMesh(false), Mesh{}, Scope{Node: "n1"}, movedFps); err == nil || !strings.Contains(err.Error(), `restic needs a secret called "repository"`) { t.Errorf("restic composed from the indexer's manifest: %v, want its own secret refused", err) } builtBus := movedMesh(true) fromBuild, _, err := Compose(ctx, builtBus, Mesh{}, Scope{Node: "n1"}, movedFps) if err != nil { t.Fatalf("restic composed from its build: %v", err) } if !reflect.DeepEqual(builtBus.fetched, []string{"http://127.0.0.1:5000/v2/restic/files/blobs/" + archiveDigest}) { t.Errorf("the templates archive was read at %v", builtBus.fetched) } resticKey, _ := delivery.ComposedKey("n1", "restic", movedFps["restic"]) if c := fromBuild[resticKey]; len(c.Filled) != 4 { // its three templated files and its own secret t.Errorf("restic is not handed to the provisioner: its filled files are %v", c.Filled) } // A done key an artifact of which names no reference is waited for, in the controller's words (ADR 0498 §8). unreferenced := movedMesh(true) unreferenced.builds["restic "+movedFps["restic"]].Artifacts = []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Digest: digestOf("6")}, resticArtifacts[1]} if _, _, err := Compose(ctx, unreferenced, Mesh{}, Scope{Node: "n1"}, movedFps); err == nil || !strings.Contains(err.Error(), "the build of restic is done and waits for the reference of tools") { t.Errorf("a done key with an unreferenced artifact: %v", err) } // A module the node runs inside its own composition (the fold) composes from the indexer's manifest though a done // build of it is at the trunk's FP2: its build is not read. folding := movedMesh(true) foldIn, err := Input(ctx, folding, Mesh{}, Scope{Node: "n1", InOwn: []string{"mesh-provisioner-client"}}) if err != nil { t.Fatal(err) } clientParsed, err := catalogue.ParseManifestForInputs(clientRaw) if err != nil { t.Fatal(err) } if slices.Contains(folding.buildsRead, "mesh-provisioner-client") || !reflect.DeepEqual(foldIn.Catalogue["mesh-provisioner-client"], clientParsed) { t.Errorf("a folded module was taken from its build: builds read %v", folding.buildsRead) } // The controller's: its take-in resolves each manifest with the key's artifacts and the archive's texts, and composes // from those over the same input. storeIn, err := Input(ctx, movedMesh(false), Mesh{}, Scope{Node: "n1"}) if err != nil { t.Fatal(err) } for module, built := range map[string][]catalogue.Built{ "restic": {{Name: "tools", Kind: "bundle", Reference: resticArtifacts[0].Reference, Digest: digestOf("6")}, {Name: "files", Kind: "archive", Reference: resticArtifacts[1].Reference, Digest: archiveDigest, Texts: texts}}, "mesh-provisioner-client": {{Name: "client", Kind: "bundle", Reference: clientArtifacts[0].Reference, Digest: digestOf("7")}}, } { resolved, err := storeIn.Catalogue[module].Resolve(built) if err != nil { t.Fatal(err) } // Kept as its bytes and read again, as the controller's store holds a build's manifest. raw, _ := json.Marshal(resolved) if storeIn.Catalogue[module], err = catalogue.ParseManifestForInputs(raw); err != nil { t.Fatal(err) } } storeIn.OwnSecrets = movedMesh(true).own fromStoreMoved, _, err := compose.Compositions(storeIn, "n1", movedFps, compose.Reading) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(fromBuild, fromStoreMoved) { gj, _ := json.MarshalIndent(fromBuild, "", " ") wj, _ := json.MarshalIndent(fromStoreMoved, "", " ") t.Errorf("composed from the build differs from the controller's take-in:\n got %s\nwant %s", gj, wj) } // And what a provider on another node serves from its settings (mailu's smtp domain, issue 1126: shanks refused // '${setting:domain} ... nothing sets "domain"'): read for that provider's served keys alone, through Served. mailer := []byte(`{"module":"mailer","version":"1","config":{"domain":{"type":"string","description":"the mail domain","required":true}},` + `"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}`) withMailer := func(set bool) *bus { b := mk() p := b.placements["anchor"] p.Modules = append(p.Modules, delivery.PlacedModule{Module: "mailer"}) b.placements["anchor"] = p b.modules["mailer"] = delivery.Module{Manifest: delivery.ManifestDigest(mailer)} b.manifests[delivery.ManifestDigest(mailer)] = mailer if set { b.served = map[string]map[string]any{"anchor.mailer": {"domain": "example.org"}} } return b } if _, _, err := Compose(ctx, withMailer(false), Mesh{}, Scope{Node: "laptop"}, fps); err == nil || !strings.Contains(err.Error(), `nothing sets "domain"`) { t.Errorf("a provider's served setting nobody sets: %v", err) } served := withMailer(true) if _, _, err := Compose(ctx, served, Mesh{}, Scope{Node: "laptop"}, fps); err != nil { t.Errorf("laptop with what anchor's mailer serves: %v", err) } if !reflect.DeepEqual(served.servedRead, []string{"anchor.mailer:domain"}) || slices.Contains(served.valuesRead, "anchor.mailer") { t.Errorf("laptop read %v of other nodes (and %v of config values): the served keys of anchor's mailer alone", served.servedRead, served.valuesRead) } for name, c := range map[string]struct { change func(*bus) scope Scope says string }{ "no record": {func(b *bus) { delete(b.records, "laptop") }, Scope{Node: "anchor"}, "laptop, whose agent has written no record"}, "no placement": {func(b *bus) {}, Scope{Node: "stranger"}, "stranger has no placement"}, "a manifest unread": {func(b *bus) { delete(b.manifests, digest) }, Scope{Node: "laptop"}, "the manifest of xterm"}, "a kept one unbuilt": {func(b *bus) {}, Scope{Node: "laptop", KeptAt: map[string]string{"xterm": "src2:gone"}}, "holds no done build"}, } { b := mk() c.change(b) if _, err := Input(ctx, b, Mesh{}, c.scope); err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%s: %v, want it to say %q", name, err, c.says) } } }