Merge branch 'b6/registry' into feat/registry-milestone

This commit is contained in:
2026-11-05 15:48:43 +01:00
52 changed files with 1332 additions and 1098 deletions
+2 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
engcomp "github.com/novox/mesh-host/internal/compositions"
"slices"
"sort"
@@ -12,7 +13,6 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose/resolution"
)
@@ -70,7 +70,7 @@ func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (ou
// not folded (issue 1585: nats on novox, folded by the reader and composed at src2: here, refused for its server
// artifact no build had published).
func Folds(inOwn bool, fp string) bool {
return inOwn && !strings.HasPrefix(fp, delivery.SourcePrefix2)
return inOwn && !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2)
}
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
+2 -17
View File
@@ -2,6 +2,7 @@ package compose
import (
"encoding/json"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
@@ -9,11 +10,10 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose/resolution"
)
func fpFor(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
func fpFor(c byte) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(string(c), 64) }
// 1943/AC5: a node's compositions are its declaration as Compose gives it, split by the module each resource belongs
// to: every resource once, each in its module's composition at the fingerprint given, the module's ownership said on
@@ -186,18 +186,3 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
}
}
}
// 1943/AC1: the upgrade policies and sources a composition's value may name are the catalogue's own words.
func TestTheComposedPoliciesAreTheCatalogues(t *testing.T) {
for _, p := range []string{catalogue.PolicyRoll, catalogue.PolicyTogether, catalogue.PolicyRecord} {
for _, from := range []string{catalogue.FromPerson, catalogue.FromModule, "the bus", catalogue.FromData, catalogue.FromDefault} { // "the bus": an older reader's word, still read
v := delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("a", 64), Judgement: delivery.JudgedNeither, Policy: p, PolicyFrom: from}
if _, err := delivery.EncodeComposedDigest(v); err != nil {
t.Errorf("%s from %s: %v", p, from, err)
}
}
}
if delivery.PolicyRecord != catalogue.PolicyRecord {
t.Errorf("delivery.PolicyRecord = %q, the catalogue's %q", delivery.PolicyRecord, catalogue.PolicyRecord)
}
}
+43 -40
View File
@@ -6,6 +6,10 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -15,7 +19,6 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
)
@@ -38,37 +41,37 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
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"}}
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
for _, m := range placed {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
b := &bus{placements: map[string]delivery.Placement{"n1": p},
b := &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
records: map[string]nodeengine.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{}}
modules: map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)}},
manifests: map[string][]byte{meshcatalogclient.ManifestDigest(lemurs): lemurs},
builds: map[string]*builds.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)}
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.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)))
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)}}}
b.builds[m+" "+mod.Fp2] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest,
Artifacts: []builds.BuiltArtifact{{Name: "site", Kind: "image", Digest: "sha256:" + strings.Repeat("b", 64)}}}
}
return b
}
given := func(b *bus) map[string]*delivery.Placement {
given := func(b *bus) map[string]*meshmoduleassigner.Placement {
p := b.placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
// Every refusal: two builds taken in, and a module the composition refuses (its image never built), each named.
@@ -203,16 +206,16 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
bp.Decided.Endpoint = "n1.example:51820"
bb.placements["n1"] = bp
sp := bb.modules["spiegel"]
sp.FP2 = fp("3")
sp.Fp2 = fp("3")
bb.modules["spiegel"] = sp
image := "registry.example:5100/spiegel/site@sha256:" + strings.Repeat("7", 64)
bb.builds["spiegel "+sp.FP2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: sp.Manifest,
Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}}
bb.builds["spiegel "+sp.Fp2] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: sp.Manifest,
Artifacts: []builds.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}}
read, err := ComposingInput(ctx, bb, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}},
map[string]string{"lemurs": fp("e"), "spiegel": sp.FP2})
map[string]string{"lemurs": fp("e"), "spiegel": sp.Fp2})
raw, _ := json.Marshal(read.Catalogue["spiegel"])
if err != nil || read.ComposedHow.Held["spiegel"] != "" || !strings.Contains(string(raw), strings.Repeat("7", 64)) {
t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.FP2, err,
t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.Fp2, err,
read.ComposedHow, raw)
}
}
@@ -231,14 +234,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
`"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)}
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.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}
p.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b.placements["n1"] = p
@@ -369,8 +372,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
`"resources":[{"id":"service","type":"service","unit":"mailu.service","state":"running","boot":"enabled"}]}`)
b = mk("lemurs", overlay.Name, "mailu", "proxy", "de-spiegel")
for m, raw := range map[string][]byte{"de-spiegel": spiegel, "proxy": proxy, "mailu": mailu} {
b.manifests[delivery.ManifestDigest(raw)] = raw
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
}
p = b.placements["n1"]
p.Decided.Endpoint = "n1.example:51820"
@@ -469,14 +472,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
`{"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)}
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.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}
p.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b.placements["n1"] = p
@@ -510,14 +513,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
`"resources":[{"id":"service","type":"service","unit":"app.service","state":"running","boot":"enabled"}]}`)
b2 := mk("lemurs", overlay.Name, "site", "nft", "app")
for m, raw := range map[string][]byte{"site": provider, "nft": nft, "app": app} {
b2.manifests[delivery.ManifestDigest(raw)] = raw
b2.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
b2.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
b2.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
}
p2 := b2.placements["n1"]
p2.Decided.Endpoint = "n1.example:51820"
for i := range p2.Modules {
if p2.Modules[i].Module == "site" {
p2.Modules[i].Ports = map[string]int{"8080": 20005}
p2.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b2.placements["n1"] = p2
@@ -553,16 +556,16 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
// build that runs (7) listens on 8080. The filter composes with what site last offered, its manifest at the build
// that runs (issue 1960: a hold never spreads), so it opens 8080 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)}
b.manifests[meshcatalogclient.ManifestDigest(moved)] = moved
b.modules["site"] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.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"]}`)
siteRuns.FP = fp("7")
b.manifests[delivery.ManifestDigest(site)] = site
b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest(site)}
b.manifests[meshcatalogclient.ManifestDigest(site)] = site
b.builds["site "+fp("7")] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(site)}
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.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 alone", err, held)
@@ -588,13 +591,13 @@ func otherNodeNotResolvedIsNamed(t *testing.T) {
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}}}},
b := &bus{placements: map[string]meshmoduleassigner.Placement{
"n1": {Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1", Endpoint: "n1.example:51820"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "cons"}, {Module: overlay.Name}}},
"n2": {Node: "n2", Decided: meshmoduleassigner.NodeDecided{Address: "10.77.0.2"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "prov"}, {Module: overlay.Name}}}},
records: map[string]nodeengine.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{}
modules: map[string]meshcatalog.ModulesByModuleValue{"prov": {Manifest: meshcatalogclient.ManifestDigest(prov)}, "cons": {Manifest: meshcatalogclient.ManifestDigest(cons)}},
manifests: map[string][]byte{meshcatalogclient.ManifestDigest(prov): prov, meshcatalogclient.ManifestDigest(cons): cons}}
given := map[string]*meshmoduleassigner.Placement{}
for n, p := range b.placements {
p := p
given[n] = &p
+18 -18
View File
@@ -2,12 +2,12 @@ package frombus
import (
"fmt"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"sort"
"strconv"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
)
@@ -16,7 +16,7 @@ import (
// answers, from its own store by its own code, exactly what the seat's adopt proposes as the seed. Its answer must stay
// stable for the same store, since adopt checks its digest again at the warrant.
// What the machine observes (its record) is not in it.
func Export(in compose.Input, node string) (delivery.Placement, error) {
func Export(in compose.Input, node string) (meshmoduleassigner.Placement, error) {
var n *compose.Node
for i := range in.Nodes {
if in.Nodes[i].Name == node {
@@ -24,14 +24,14 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
}
if n == nil {
return delivery.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
return meshmoduleassigner.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
}
// At the version a placement key is read at, with a list of modules even when none is placed: the export is the
// seed's value, and the reader refuses a value of version 0 or without its modules.
p := delivery.Placement{V: sdkkeys.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{
p := meshmoduleassigner.Placement{V: sdkkeys.Version, Node: node, Modules: []meshmoduleassigner.PlacedModule{}, Decided: meshmoduleassigner.NodeDecided{
Endpoint: n.Place.Endpoint, Site: n.Place.Site, Hub: n.Place.Hub, Address: n.Place.Address,
PublicDomain: n.PublicDomain, Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount,
AgentHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
AgentAccountHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
// The tunnel found at the node's adoption: the one its private network took over, or, on the hub, the mesh's adopted
// one whether or not the hub took it; and the names a person gave the hub's carried peers.
if t, ok := in.Tunnels[node]; ok {
@@ -52,27 +52,27 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
}
for _, module := range n.Assigned {
m := delivery.PlacedModule{Module: module}
m := meshmoduleassigner.PlacedModule{Module: module}
for port, at := range n.Ports[module] {
if m.Ports == nil {
m.Ports = map[string]int{}
m.Ports = map[string]int64{}
}
m.Ports[strconv.Itoa(port)] = at
m.Ports[strconv.Itoa(port)] = int64(at)
}
for provision, to := range n.Bound[module] {
if m.Bindings == nil {
m.Bindings = map[string]delivery.Provider{}
m.Bindings = map[string]meshmoduleassigner.Provider{}
}
m.Bindings[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
m.Bindings[provision] = meshmoduleassigner.Provider{Node: to.Node, Module: to.Module}
}
// A module in the settings form keeps its config values in its layers, not in mesh-config: they are exported with
// the placement. A module in the config form has mesh-config's values laid into its layer, which are not.
settingsForm := !in.Catalogue[module].DeclaresConfig()
for _, layer := range n.Settings[module] {
if layer.From == catalogue.MeshWideLayer {
m.HasMesh, m.MeshWide = true, layer.Placement
m.HasMeshWide, m.MeshWide = true, layer.Placement
if settingsForm && len(layer.Values) > 0 {
m.MeshValues = layer.Values
m.MeshWideValues = layer.Values
}
} else {
m.HasLayer, m.Layer = true, layer.Placement
@@ -85,13 +85,13 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
for provision, to := range n.Pins {
if p.Pins == nil {
p.Pins = map[string]delivery.Provider{}
p.Pins = map[string]meshmoduleassigner.Provider{}
}
p.Pins[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
p.Pins[provision] = meshmoduleassigner.Provider{Node: to.Node, Module: to.Module}
}
for _, h := range in.Holdings {
if h.Node == node {
p.Holds = append(p.Holds, delivery.Holding{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
p.Holds = append(p.Holds, meshmoduleassigner.Holding{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
}
}
sort.SliceStable(p.Holds, func(i, j int) bool {
@@ -103,11 +103,11 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
return p, nil
}
func recordTunnelOf(t compose.Tunnel) delivery.RecordTunnel {
out := delivery.RecordTunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
func recordTunnelOf(t compose.Tunnel) meshmoduleassigner.FoundTunnel {
out := meshmoduleassigner.FoundTunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: int64(t.Port), Mtu: int64(t.MTU),
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
for _, peer := range t.Peers {
out.Peers = append(out.Peers, delivery.RecordTunnelPeer{PublicKey: peer.PublicKey, Address: peer.Address})
out.Peers = append(out.Peers, meshmoduleassigner.FoundTunnelPeer{PublicKey: peer.PublicKey, Address: peer.Address})
}
return out
}
+56 -51
View File
@@ -36,6 +36,12 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"net/netip"
@@ -46,7 +52,6 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
"github.com/novox/mesh-host/internal/takein"
@@ -55,14 +60,14 @@ import (
// Reader is the bus as a caller reads it with its own credential. Each method reads one part's state; an error is a
// read that failed, never an absent key (an absent key is an empty answer).
type Reader interface {
// Placements is every node's placement, by node (delivery.PlacementBucket).
Placements(ctx context.Context) (map[string]delivery.Placement, error)
// Placements is every node's placement, by node (buckets.Placement).
Placements(ctx context.Context) (map[string]meshmoduleassigner.Placement, error)
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
// buckets.Have (ADR 0555 §3).
Records(ctx context.Context) (map[string]nodeengine.NodeRecord, error)
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
Modules(ctx context.Context) (map[string]delivery.Module, error)
// Manifest is the raw manifest of a digest (delivery.ManifestsBucket).
// Modules is every module the indexer holds, by module (buckets.Modules).
Modules(ctx context.Context) (map[string]meshcatalog.ModulesByModuleValue, error)
// Manifest is the raw manifest of a digest (buckets.Manifests).
Manifest(ctx context.Context, digest string) ([]byte, error)
// Values is one module's config values on one node as mesh-config resolved them (mesh-config's state `resolved`,
// keys `<node>.<module>.<key>`), decoded; nil when it has none.
@@ -74,9 +79,9 @@ type Reader interface {
// and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key
// the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it.
Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error)
// Build is a module's build at a fingerprint, the builder's key (delivery.BuilderBuildsBucket, BuilderBuildKey); nil
// Build is a module's build at a fingerprint, the builder's key (builds.BuilderBuildsBucket, BuilderBuildKey); nil
// when the builder holds none. The composing node composes each of its modules from its build (issue 1126).
Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error)
Build(ctx context.Context, module, fp string) (*builds.BuilderBuild, error)
// Archive is the archive at url, a kept reference routed to the artifact store as the composing node reaches it
// (catalogue.Routed), checked against digest: a moved module's templates archive, read back for its texts as the
// controller's take-in reads it (takein.Built).
@@ -88,11 +93,11 @@ type Reader interface {
Served(ctx context.Context, node, module string, keys []string) (map[string]any, error)
}
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (delivery.SignersBucket,
// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (buckets.Signers,
// key meshvault.SignersByAuthorityKey(); ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
// controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before.
type SignersReader interface {
// VaultAuthority is the vault's authority certificate as PEM, checked (delivery.DecodeSignerAuthority); empty when the
// VaultAuthority is the vault's authority certificate as PEM, checked (meshvaultclient.DecodeSignerAuthority); empty when the
// vault has published none. A read that fails is said by the reader and answered empty: a node that may not read the
// state yet composes as before rather than not at all.
VaultAuthority(ctx context.Context) string
@@ -133,7 +138,7 @@ type Mesh struct {
// read it: the check of every node judges placement, and reads no archive.
//
// Placements, when set, stands in for the placement of the nodes it names: a proposal's candidates
// (delivery.PlacementCandidate), so a node composes in shadow with a proposal laid over and compares the result with
// (meshmoduleassigner.CandidatesByProposalNodeValue), so a node composes in shadow with a proposal laid over and compares the result with
// what it runs (ADR 0567 §3); a nil value removes the node's placement. The rest is read as always.
//
// InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again
@@ -146,7 +151,7 @@ type Scope struct {
Node string
Every bool
KeptAt map[string]string
Placements map[string]*delivery.Placement
Placements map[string]*meshmoduleassigner.Placement
InOwn []string
Running []engcomp.Composed
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
@@ -167,7 +172,7 @@ type Scope struct {
// controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module
// run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own
// composition keeps them as it runs them until a want names a build of the new form for one.
func InOwn(running []engcomp.Composed, p delivery.Placement) []string {
func InOwn(running []engcomp.Composed, p meshmoduleassigner.Placement) []string {
placed := map[string]bool{}
for _, m := range p.Modules {
placed[m.Module] = true
@@ -196,7 +201,7 @@ func InOwn(running []engcomp.Composed, p delivery.Placement) []string {
// Input is the composition input for s, read through r.
//
// **A refused manifest never ends the read** (novox/hq ADR 0578 §3): each manifest is decoded leniently
// (catalogue.Decode) and the indexer's verdict on it taken from its modules value (delivery.TakeManifestVerdict, a missing
// (catalogue.Decode) and the indexer's verdict on it taken from its modules value (meshcatalogclient.TakeVerdict, a missing
// one accepted); a module whose manifest the indexer's judge refused, or whose bytes do not decode, stands in the
// catalogue as its stub (unusable), so what it provides and requires still shapes the rest, and is named with why in
// the input's Refused; a field set aside is named in its SetAside. Every other module of the read goes on.
@@ -207,10 +212,10 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
// taken is a module's manifest as a reader takes it (novox/hq ADR 0578 §3): decoded leniently, with the indexer's verdict
// from its modules value. A refused one, or bytes that do not decode, stands as its stub (unusable) and why says so; a
// manifest not read past a field it set aside holds its name alone and is held the same.
func taken(name string, raw []byte, m delivery.Module) (man catalogue.Manifest, why string) {
if v := m.VerdictOf(name); v != nil {
if refused, _ := delivery.TakeManifestVerdict(v); refused {
return unusable(name, raw), v.Why()
func taken(name string, raw []byte, m meshcatalog.ModulesByModuleValue) (man catalogue.Manifest, why string) {
if v := meshcatalogclient.ModuleVerdict(name, m); v != nil {
if refused, _ := meshcatalogclient.TakeVerdict(v); refused {
return unusable(name, raw), meshcatalogclient.VerdictWhy(*v)
}
}
man, err := catalogue.Decode(raw)
@@ -252,9 +257,9 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
// for its plans at the wanted builds and at the newest, whose builds alone differ (PlansHeldTwice).
type base struct {
in compose.Input
placements map[string]delivery.Placement
placements map[string]meshmoduleassigner.Placement
records map[string]nodeengine.NodeRecord
modules map[string]delivery.Module
modules map[string]meshcatalog.ModulesByModuleValue
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
// read: every scope holds it.
ownRefused [][2]string
@@ -292,7 +297,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
return b, fmt.Errorf("frombus: placement: %w", err)
}
if len(s.Placements) > 0 {
given := make(map[string]delivery.Placement, len(placements)+len(s.Placements))
given := make(map[string]meshmoduleassigner.Placement, len(placements)+len(s.Placements))
for node, p := range placements {
given[node] = p
}
@@ -340,7 +345,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
// against the bundle. The controller's stays while a certificate it issued is in a node's record.
if sr, ok := r.(SignersReader); ok {
if vault := sr.VaultAuthority(ctx); vault != "" {
in.Authority = delivery.AuthorityBundle(in.Authority, vault)
in.Authority = meshvaultclient.AuthorityBundle(in.Authority, vault)
}
}
named := func(field *map[string]string, module, why string) {
@@ -381,7 +386,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
// The module the mesh provides itself (the private network) has no repository the indexer reads: its manifest comes
// with mesh-sdk and always wins, as the controller's providedModules and the grants part have it.
for _, name := range delivery.ProvidedModules() {
for _, name := range overlay.ProvidedModules() {
raw, err := json.Marshal(overlay.Manifest())
if err != nil {
return b, err
@@ -419,7 +424,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
strings.Join(keys, ", "), err)
}
}
if layers := m.Layers(node, values); layers != nil {
if layers := meshmoduleassignerclient.Layers(m, node, values); layers != nil {
if n.Settings == nil {
n.Settings = map[string][]catalogue.Layer{}
}
@@ -468,7 +473,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
}
}
in.Nodes = append(in.Nodes, n)
in.Holdings = append(in.Holdings, p.Held()...)
in.Holdings = append(in.Holdings, meshmoduleassignerclient.Held(p)...)
// A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the
// controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over
// (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers).
@@ -497,7 +502,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
// finish is input from its base on: the composing node's modules from their builds (build reads one), its own secrets.
// in is the base's input, or a copy of it (base.cloned) where one base is finished more than once.
func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error),
func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error),
mesh Mesh, s Scope, in compose.Input, refuse func(module string, err error), wanted map[string]string) (compose.Input, error) {
var err error
parse := parser(mesh)
@@ -773,7 +778,7 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed,
continue
}
b, err := r.Build(ctx, module, c.FP)
if err != nil || b == nil || b.State != delivery.BuildDone || b.Manifest == "" {
if err != nil || b == nil || b.State != builds.BuildDone || b.Manifest == "" {
return catalogue.Manifest{}, false
}
raw, err := r.Manifest(ctx, b.Manifest)
@@ -791,8 +796,8 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed,
// module it keeps at another build (Scope.KeptAt, into its node's Kept), read from the builder's done key and resolved
// by the one take-in rule (takein.Manifest), a templates archive read in the artifact store as the node reaches it
// (issue 1126).
func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error),
in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error),
in *compose.Input, s Scope, p meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue,
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
refuse func(module string, err error), wanted map[string]string) error {
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
@@ -831,7 +836,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
}
return r.Archive(ctx, catalogue.Routed(reference, address), digest)
}
built := func(module, fp string, b *delivery.BuilderBuild) (catalogue.Manifest, error) {
built := func(module, fp string, b *builds.BuilderBuild) (catalogue.Manifest, error) {
if b.Manifest == "" {
return catalogue.Manifest{}, fmt.Errorf("frombus: the build of %s at %s names no manifest", module, fp)
}
@@ -879,7 +884,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
// the scope keeps at another build is.
keptAt := maps.Clone(s.KeptAt)
for _, pm := range p.Modules {
if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 &&
if m, ok := modules[pm.Module]; ok && m.Fp2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.Fp2 &&
keptAt[pm.Module] == "" && !compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
if keptAt == nil {
keptAt = map[string]string{}
@@ -895,7 +900,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
}
for _, pm := range p.Modules {
m, ok := modules[pm.Module]
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
if !ok || m.Fp2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
// A module it runs inside its own composition is composed there as before while it is folded: no build of the
// new form is what it runs (Scope.InOwn). Wanted at a build of the new form it is no longer folded (compose.Folds,
// the composition's own rule), so its build is taken in (issue 1585).
@@ -907,26 +912,26 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) {
continue // nothing is built of it: its manifest is what it composes from
}
b, err := build(ctx, pm.Module, m.FP2)
b, err := build(ctx, pm.Module, m.Fp2)
if err != nil {
err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err)
err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.Fp2, err)
if refuse == nil {
return err
}
stub(pm.Module, err)
continue
}
if b == nil || b.State != delivery.BuildDone {
if b == nil || b.State != builds.BuildDone {
if refuse != nil && runs[pm.Module] {
// A module that runs is never composed from the indexer's manifest, which names no build's artifacts (no
// runtime's bundle): it is held, keeping what runs, until its build lands (issue 774, 2026-10-30 19:30:
// node-tools on ace, g14 and shanks, "its build produced 0 bundle(s)", and every module refused).
stub(pm.Module, fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build yet; it "+
"keeps running what it runs", pm.Module, m.FP2))
"keeps running what it runs", pm.Module, m.Fp2))
}
continue // nothing of it runs: composed from the indexer's manifest, as before
}
resolved, err := built(pm.Module, m.FP2, b)
resolved, err := built(pm.Module, m.Fp2, b)
if err != nil {
if refuse == nil {
return err
@@ -946,7 +951,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
switch {
case err != nil:
err = fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err)
case b == nil || b.State != delivery.BuildDone:
case b == nil || b.State != builds.BuildDone:
err = fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp)
}
var resolved catalogue.Manifest
@@ -999,7 +1004,7 @@ func attachedOf(in []nodeengine.AttachedNetwork) []catalogue.AttachedNetwork {
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
// decided of it, and where its modules run).
func nodeOf(rec nodeengine.NodeRecord, p delivery.Placement) compose.Node {
func nodeOf(rec nodeengine.NodeRecord, p meshmoduleassigner.Placement) compose.Node {
n := compose.Node{
Name: rec.Node,
Place: compose.Place{Key: rec.TunnelKey},
@@ -1019,13 +1024,13 @@ func nodeOf(rec nodeengine.NodeRecord, p delivery.Placement) compose.Node {
// placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes
// (its keys, links, networks, certificate) and its settings layers are left as they are.
func placeNode(n *compose.Node, p delivery.Placement) {
func placeNode(n *compose.Node, p meshmoduleassigner.Placement) {
d := p.Decided
n.Place = compose.Place{Key: n.Place.Key, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address}
n.Assigned, n.PublicDomain = p.Assigned(), d.PublicDomain
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentHome
n.Assigned, n.PublicDomain = meshmoduleassignerclient.Assigned(p), d.PublicDomain
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentAccountHome
n.Adopted, n.Taken = d.Adopted, d.Taken
n.Pins, n.Bound, n.Ports = p.ChosenPins(), p.Bound(), p.HostPorts()
n.Pins, n.Bound, n.Ports = meshmoduleassignerclient.ChosenPins(p), meshmoduleassignerclient.Bound(p), meshmoduleassignerclient.HostPorts(p)
}
// Placed is in with the placements given standing in for those of the nodes they name (issue 1126): each such node's
@@ -1035,7 +1040,7 @@ func placeNode(n *compose.Node, p delivery.Placement) {
// no record to compose it from. The found tunnel and the hub's peers are read from in as they are, not from the given
// placement. It is how a check composes a change's own placement over an input already filled (composer.Case's
// Placements).
func Placed(in compose.Input, placements map[string]*delivery.Placement) (compose.Input, error) {
func Placed(in compose.Input, placements map[string]*meshmoduleassigner.Placement) (compose.Input, error) {
if len(placements) == 0 {
return in, nil
}
@@ -1052,7 +1057,7 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node)
}
if p == nil {
p = &delivery.Placement{Node: node}
p = &meshmoduleassigner.Placement{Node: node}
}
if p.Node != node {
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
@@ -1067,7 +1072,7 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
values = l.Values
}
}
if layers := m.Layers(node, values); layers != nil {
if layers := meshmoduleassignerclient.Layers(m, node, values); layers != nil {
settings[m.Module] = layers
}
}
@@ -1085,14 +1090,14 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
}
for _, node := range sortedKeys(placements) {
if p := placements[node]; p != nil {
out.Holdings = append(out.Holdings, p.Held()...)
out.Holdings = append(out.Holdings, meshmoduleassignerclient.Held(*p)...)
}
}
return out, nil
}
func tunnelOf(t delivery.RecordTunnel) compose.Tunnel {
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
func tunnelOf(t meshmoduleassigner.FoundTunnel) compose.Tunnel {
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: int(t.Port), MTU: int(t.Mtu),
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
for _, p := range t.Peers {
out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
@@ -1126,7 +1131,7 @@ func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]
}
// nodesOf is every node named by a placement or a record, by name: the order the controller reads them in.
func nodesOf(placements map[string]delivery.Placement, records map[string]nodeengine.NodeRecord) []string {
func nodesOf(placements map[string]meshmoduleassigner.Placement, records map[string]nodeengine.NodeRecord) []string {
seen := map[string]bool{}
for n := range placements {
seen[n] = true
+70 -64
View File
@@ -13,6 +13,11 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -23,7 +28,6 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
)
@@ -31,9 +35,9 @@ import (
type bus struct {
vaultAuthority string
handed map[string]bool
placements map[string]delivery.Placement
placements map[string]meshmoduleassigner.Placement
records map[string]nodeengine.NodeRecord
modules map[string]delivery.Module
modules map[string]meshcatalog.ModulesByModuleValue
manifests map[string][]byte
values map[string]map[string]any // by node.module
valuesRead []string
@@ -44,7 +48,7 @@ type bus struct {
asked []compose.CredentialKey
// builds are the builder's keys, by "<module> <fp>"; archives the artifact store's blobs, by digest, and fetched
// every URL asked; served another node's values by "<node>.<module>", and servedRead every ask with its keys.
builds map[string]*delivery.BuilderBuild
builds map[string]*builds.BuilderBuild
archives map[string][]byte
fetched []string
served map[string]map[string]any
@@ -54,7 +58,7 @@ type bus struct {
held bool
}
func (b *bus) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) {
func (b *bus) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) {
b.buildsRead = append(b.buildsRead, module)
return b.builds[module+" "+fp], nil
}
@@ -79,7 +83,7 @@ func (b *bus) Served(_ context.Context, node, module string, keys []string) (map
return out, nil
}
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
func (b *bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
return b.placements, nil
}
func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority }
@@ -87,7 +91,9 @@ func (b *bus) Handed(context.Context, string) map[string]bool { return b.handed
func (b *bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
return b.records, nil
}
func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil }
func (b *bus) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) {
return b.modules, nil
}
func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) {
raw, ok := b.manifests[digest]
if !ok {
@@ -159,35 +165,35 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
digest := delivery.ManifestDigest(xterm)
digest := meshcatalogclient.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)
mediaDigest := meshcatalogclient.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"}}}
tunnel := &meshmoduleassigner.FoundTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
Peers: []meshmoduleassigner.FoundTunnelPeer{{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",
placements: map[string]meshmoduleassigner.Placement{
"anchor": {Node: "anchor", Decided: meshmoduleassigner.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,
Modules: []meshmoduleassigner.PlacedModule{{Module: overlay.Name}},
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}},
"laptop": {Node: "laptop", Decided: meshmoduleassigner.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
Modules: []meshmoduleassigner.PlacedModule{{Module: overlay.Name},
{Module: "xterm", HasLayer: true, Ports: map[string]int64{"80": 8080}},
{Module: "media", HasMeshWide: true, MeshWideValues: 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]nodeengine.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": {}},
modules: map[string]meshcatalog.ModulesByModuleValue{"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"}},
@@ -291,7 +297,7 @@ func TestInput(t *testing.T) {
running = append(running, c)
}
laptop := mk().placements["laptop"]
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps)
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop}}, fps)
if err == nil && len(held) > 0 {
err = fmt.Errorf("modules held: %v", held)
}
@@ -303,7 +309,7 @@ func TestInput(t *testing.T) {
}
moved := mk().placements["anchor"]
moved.Decided.Endpoint = "anchor.example:51821"
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
if err == nil && len(held) > 0 {
err = fmt.Errorf("modules held: %v", held)
}
@@ -329,16 +335,16 @@ func TestInput(t *testing.T) {
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"}}
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
for _, m := range placed {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
return &bus{placements: map[string]delivery.Placement{"n1": p},
return &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
records: map[string]nodeengine.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},
modules: map[string]meshcatalog.ModulesByModuleValue{"keeper": {Manifest: meshcatalogclient.ManifestDigest(keeper)}, "lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)},
"objects": {Manifest: meshcatalogclient.ManifestDigest(objects)}, "files": {Manifest: meshcatalogclient.ManifestDigest(files)}},
manifests: map[string][]byte{meshcatalogclient.ManifestDigest(keeper): keeper, meshcatalogclient.ManifestDigest(lemurs): lemurs,
meshcatalogclient.ManifestDigest(objects): objects, meshcatalogclient.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),
@@ -384,9 +390,9 @@ func TestInput(t *testing.T) {
b.placements["n1"] = p
return b
}
tunnelledAs := func() map[string]*delivery.Placement {
tunnelledAs := func() map[string]*meshmoduleassigner.Placement {
p := tunnelled().placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
folded := func() []compositions.Composed {
b := tunnelled()
@@ -434,13 +440,13 @@ func TestInput(t *testing.T) {
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 {
placedAs := func(modules ...string) map[string]*meshmoduleassigner.Placement {
p := one(modules...).placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
for name, c := range map[string]struct {
b *bus
given map[string]*delivery.Placement
given map[string]*meshmoduleassigner.Placement
running []compositions.Composed
verdict string
check func(nodeengine.CandidateAnswer) bool
@@ -477,7 +483,7 @@ func TestInput(t *testing.T) {
runsNow, nodeengine.CandidateAnswerVerdictRefused, func(a nodeengine.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, nodeengine.CandidateAnswerVerdictRefused,
"cannot compose": {one("keeper"), map[string]*meshmoduleassigner.Placement{"n1": {Node: "n2"}}, runsNow, nodeengine.CandidateAnswerVerdictRefused,
func(a nodeengine.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
@@ -497,16 +503,16 @@ func TestInput(t *testing.T) {
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) {
if got, err := Placed(want, map[string]*meshmoduleassigner.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"] {
if got, err := Placed(want, map[string]*meshmoduleassigner.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 {
if _, err := Placed(want, map[string]*meshmoduleassigner.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 {
if _, err := Input(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": {Node: "anchor"}}}); err == nil {
t.Error("a given placement naming another node was taken")
}
@@ -517,8 +523,8 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if gj, _ := delivery.EncodePlacement(got); !delivery.SamePlacement(got, placed) {
pj, _ := delivery.EncodePlacement(placed)
if gj, _ := meshmoduleassignerclient.EncodePlacement(got); !meshmoduleassignerclient.SamePlacement(got, placed) {
pj, _ := meshmoduleassignerclient.EncodePlacement(placed)
t.Errorf("%s exported as %s, want %s", node, gj, pj)
}
}
@@ -554,8 +560,8 @@ func TestInput(t *testing.T) {
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)}}}}
kept.builds = map[string]*builds.BuilderBuild{"xterm " + oldFP: {State: builds.BuildDone, Manifest: "old",
Artifacts: []builds.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: toolsRef, Digest: "sha256:" + strings.Repeat("1", 64)}}}}
parsedOld, err := catalogue.Decode(oldXterm)
if err != nil {
t.Fatal(err)
@@ -607,35 +613,35 @@ func TestInput(t *testing.T) {
if err := zw.Close(); err != nil {
t.Fatal(err)
}
archiveDigest := delivery.ManifestDigest(packed.Bytes())
archiveDigest := meshcatalogclient.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")},
resticArtifacts := []builds.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")}}
clientArtifacts := []builds.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.Modules = []meshmoduleassigner.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.modules = map[string]meshcatalog.ModulesByModuleValue{}
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
b.modules[module] = meshcatalog.ModulesByModuleValue{Fp2: movedFps[module], Manifest: meshcatalogclient.ManifestDigest(raw)}
b.manifests[meshcatalogclient.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.builds = map[string]*builds.BuilderBuild{
"restic " + movedFps["restic"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: resticArtifacts},
"mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), Artifacts: clientArtifacts},
"store " + movedFps["store"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)},
}
b.archives = map[string][]byte{archiveDigest: packed.Bytes()}
}
@@ -668,7 +674,7 @@ func TestInput(t *testing.T) {
}
// 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]}
unreferenced.builds["restic "+movedFps["restic"]].Artifacts = []builds.BuiltArtifact{{Name: "tools", Kind: "bundle", Digest: digestOf("6")}, resticArtifacts[1]}
if _, _, held, err := Compose(ctx, unreferenced, Mesh{}, Scope{Node: "n1"}, movedFps); err != nil || !strings.Contains(held["restic"], "the build of restic is done and waits for the reference of tools") {
t.Errorf("a done key with an unreferenced artifact: %v %v, want restic held alone", err, held)
}
@@ -766,10 +772,10 @@ func TestInput(t *testing.T) {
withMailer := func(set bool) *bus {
b := mk()
p := b.placements["anchor"]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "mailer"})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "mailer"})
b.placements["anchor"] = p
b.modules["mailer"] = delivery.Module{Manifest: delivery.ManifestDigest(mailer)}
b.manifests[delivery.ManifestDigest(mailer)] = mailer
b.modules["mailer"] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(mailer)}
b.manifests[meshcatalogclient.ManifestDigest(mailer)] = mailer
if set {
b.served = map[string]map[string]any{"anchor.mailer": {"domain": "example.org"}}
}
@@ -836,7 +842,7 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
}
placements, _ := b.Placements(ctx)
for _, p := range placements {
values, err := delivery.PlacementValues(p)
values, err := meshmoduleassignerclient.PlacementValues(p)
for k, raw := range values {
put(PlacementRead, k, raw, err)
}
@@ -850,18 +856,18 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
}
modules, _ := b.Modules(ctx)
for name, m := range modules {
raw, err := delivery.EncodeModule(m)
raw, err := meshcatalogclient.EncodeModule(m)
put(ModulesRead, name, raw, err)
var mf struct{ Module string }
_ = json.Unmarshal(b.manifests[m.Manifest], &mf)
k, _ := delivery.ManifestsKey(m.Manifest)
raw, err = delivery.EncodeManifest(delivery.Manifest{Module: mf.Module, JSON: string(b.manifests[m.Manifest])})
k, _ := meshcatalogclient.ManifestsKey(m.Manifest)
raw, err = meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: mf.Module, Manifest: string(b.manifests[m.Manifest])})
put(ManifestsRead, k, raw, err)
}
for key, build := range b.builds {
module, fp, _ := strings.Cut(key, " ")
k, _ := delivery.BuilderBuildKey(module, fp)
raw, err := delivery.EncodeBuilderBuild(*build)
k, _ := builds.BuilderBuildKey(module, fp)
raw, err := builds.EncodeBuilderBuild(*build)
put(BuildsRead, k, raw, err)
}
for nm, values := range b.values {
@@ -13,11 +13,13 @@ import (
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/frombus"
"strings"
)
//go:embed testdata
@@ -25,28 +27,30 @@ var testdata embed.FS
// Bus is a frombus.Reader over values held in memory. A credential asked is answered held when Held is set.
type Bus struct {
Placement map[string]delivery.Placement
Placement map[string]meshmoduleassigner.Placement
Record map[string]nodeengine.NodeRecord
Module map[string]delivery.Module
Module map[string]meshcatalog.ModulesByModuleValue
Manifests map[string][]byte
Value map[string]map[string]any // by node.module
Own []compose.OwnSecretRow
Builds map[string]*delivery.BuilderBuild // by "<module> <fp>"
Archives map[string][]byte // by digest
Serves map[string]map[string]any // another node's values, by node.module
Builds map[string]*builds.BuilderBuild // by "<module> <fp>"
Archives map[string][]byte // by digest
Serves map[string]map[string]any // another node's values, by node.module
Held bool
}
var _ frombus.Reader = (*Bus)(nil)
func (b *Bus) Placements(context.Context) (map[string]delivery.Placement, error) {
func (b *Bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
return b.Placement, nil
}
func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
return b.Record, nil
}
func (b *Bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.Module, nil }
func (b *Bus) VaultAuthority(context.Context) string { return "" }
func (b *Bus) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) {
return b.Module, nil
}
func (b *Bus) VaultAuthority(context.Context) string { return "" }
func (b *Bus) Manifest(_ context.Context, digest string) ([]byte, error) {
raw, ok := b.Manifests[digest]
@@ -76,7 +80,7 @@ func (b *Bus) Credentials(_ context.Context, _ string, asConsumer, asProvider []
return out, issued, nil
}
func (b *Bus) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) {
func (b *Bus) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) {
return b.Builds[module+" "+fp], nil
}
@@ -148,33 +152,33 @@ func Provisioned() (*Bus, error) {
if err := zw.Close(); err != nil {
return nil, err
}
archive := delivery.ManifestDigest(packed.Bytes())
archive := meshcatalogclient.ManifestDigest(packed.Bytes())
digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
fps := FPs()
b := &Bus{
Placement: map[string]delivery.Placement{Node: {Node: Node,
Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"},
Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "postgres"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}}},
Placement: map[string]meshmoduleassigner.Placement{Node: {Node: Node,
Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"},
Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}, {Module: "postgres"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}}},
Record: map[string]nodeengine.NodeRecord{Node: {Node: Node, TunnelKey: "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=",
SealingKey: "sk", Contract: compose.HandedContract, Capabilities: []string{"systemd", "package-manager"}}},
Module: map[string]delivery.Module{}, Manifests: map[string][]byte{},
Module: map[string]meshcatalog.ModulesByModuleValue{}, Manifests: map[string][]byte{},
Own: []compose.OwnSecretRow{{Node: Node, Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}},
Builds: map[string]*delivery.BuilderBuild{
"restic " + fps["restic"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(resticRaw), Artifacts: []delivery.BuiltArtifact{
Builds: map[string]*builds.BuilderBuild{
"restic " + fps["restic"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: []builds.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/" + archive, Digest: archive}}},
"mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(clientRaw),
Artifacts: []delivery.BuiltArtifact{{Name: "client", Kind: "bundle",
"mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw),
Artifacts: []builds.BuiltArtifact{{Name: "client", Kind: "bundle",
Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}}},
"store " + fps["store"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(store)},
"postgres " + fps["postgres"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(postgres)},
"store " + fps["store"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)},
"postgres " + fps["postgres"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(postgres)},
},
Archives: map[string][]byte{archive: packed.Bytes()},
Held: true,
}
for module, raw := range map[string][]byte{"restic": resticRaw, "mesh-provisioner-client": clientRaw, "store": store, "postgres": postgres} {
b.Module[module] = delivery.Module{FP2: fps[module], Manifest: delivery.ManifestDigest(raw)}
b.Manifests[delivery.ManifestDigest(raw)] = raw
b.Module[module] = meshcatalog.ModulesByModuleValue{Fp2: fps[module], Manifest: meshcatalogclient.ManifestDigest(raw)}
b.Manifests[meshcatalogclient.ManifestDigest(raw)] = raw
}
return b, nil
}
@@ -5,6 +5,11 @@ import (
"encoding/json"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
engcomp "github.com/novox/mesh-host/internal/compositions"
"reflect"
"slices"
@@ -13,7 +18,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose/frombus"
"github.com/novox/mesh-host/internal/compose/fromruntime"
)
@@ -67,21 +71,21 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
if row.bad != "" {
if !row.elsewhere {
p := b.Placement[Node]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "backup"})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "backup"})
b.Placement[Node] = p
}
digest := delivery.ManifestDigest([]byte(row.bad))
digest := meshcatalogclient.ManifestDigest([]byte(row.bad))
b.Manifests[digest] = []byte(row.bad)
b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest}
b.Module["backup"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest}
if row.refused != "" {
b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest, Verdict: delivery.VerdictRefused,
b.Module["backup"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Verdict: meshcatalogclient.VerdictRefused,
Problems: []string{row.refused}}
}
if row.built != "" {
builtDigest := delivery.ManifestDigest([]byte(row.built))
builtDigest := meshcatalogclient.ManifestDigest([]byte(row.built))
b.Manifests[builtDigest] = []byte(row.built)
b.Builds["backup "+fp] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: builtDigest,
Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}}
b.Builds["backup "+fp] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: builtDigest,
Artifacts: []builds.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}}
}
if row.dataOnly {
items, _, iheld, err := frombus.DataItemsHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
@@ -263,21 +267,21 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
delete(r.(map[string]any), "user")
}
raw, _ := json.Marshal(m)
mod.Manifest = delivery.ManifestDigest(raw)
mod.Manifest = meshcatalogclient.ManifestDigest(raw)
bb.Module["mesh-provisioner-client"], bb.Manifests[mod.Manifest] = mod, raw
built := *bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]]
built.Manifest = mod.Manifest
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &built
want = ""
} else if row.filledHeld {
bb.Builds["restic "+fps["restic"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding}
bb.Builds["restic "+fps["restic"]] = &builds.BuilderBuild{State: builds.BuildBuilding}
} else if row.placed {
// held: its build not done while it runs
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding}
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &builds.BuilderBuild{State: builds.BuildBuilding}
} else {
// taken out of the node's placement while what runs still names it: the account goes with it, never kept stale
p := bb.Placement[Node]
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m delivery.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m meshmoduleassigner.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
bb.Placement[Node] = p
want = ""
}
@@ -311,7 +315,7 @@ func statesOf(t *testing.T, b *Bus) frombus.States {
s[state][key] = raw
}
for node, p := range b.Placement {
raw, err := delivery.EncodePlacement(p)
raw, err := meshmoduleassignerclient.EncodePlacement(p)
put(frombus.PlacementRead, "placement."+node, raw, err)
}
for node, rec := range b.Record {
@@ -319,19 +323,19 @@ func statesOf(t *testing.T, b *Bus) frombus.States {
put(frombus.HaveRead, node+".node", raw, err)
}
for module, m := range b.Module {
key, _ := delivery.ModulesKey(module)
raw, err := delivery.EncodeModule(m)
key, _ := meshcatalogclient.ModulesKey(module)
raw, err := meshcatalogclient.EncodeModule(m)
put(frombus.ModulesRead, key, raw, err)
}
for digest, man := range b.Manifests {
key, _ := delivery.ManifestsKey(digest)
raw, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: string(man)})
key, _ := meshcatalogclient.ManifestsKey(digest)
raw, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "m", Manifest: string(man)})
put(frombus.ManifestsRead, key, raw, err)
}
for at, build := range b.Builds {
module, fp, _ := strings.Cut(at, " ")
key, _ := delivery.BuilderBuildKey(module, fp)
raw, err := delivery.EncodeBuilderBuild(*build)
key, _ := builds.BuilderBuildKey(module, fp)
raw, err := builds.EncodeBuilderBuild(*build)
put(frombus.BuildsRead, key, raw, err)
}
return s
+23 -21
View File
@@ -6,30 +6,32 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
engcomp "github.com/novox/mesh-host/internal/compositions"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
)
// aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given.
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
t.Helper()
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
b := &bus{records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}},
modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}}
modules: map[string]meshcatalog.ModulesByModuleValue{}, manifests: map[string][]byte{}, builds: map[string]*builds.BuilderBuild{}}
fps := map[string]string{}
for i, raw := range manifests {
module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: module})
b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))}
b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw)
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: module})
b.modules[module] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest([]byte(raw))}
b.manifests[meshcatalogclient.ManifestDigest([]byte(raw))] = []byte(raw)
fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64)
}
b.placements = map[string]delivery.Placement{"n1": p}
b.placements = map[string]meshmoduleassigner.Placement{"n1": p}
return b, fps
}
@@ -50,7 +52,7 @@ func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) {
// refuse writes the indexer's refusal of module's manifest into its modules value (novox/hq ADR 0578 §1).
func refuse(b *bus, module string, problems ...string) {
m := b.modules[module]
m.Verdict, m.Problems = delivery.VerdictRefused, problems
m.Verdict, m.Problems = meshcatalogclient.VerdictRefused, problems
b.modules[module] = m
}
@@ -143,14 +145,14 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) {
b.held = true // every grant held, so a contribution's credential stands in for nothing
b.own = []compose.OwnSecretRow{{Node: "n1", Module: "node-tools", Name: "broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}
mod := b.modules["node-tools"]
mod.FP2 = next
mod.Fp2 = next
b.modules["node-tools"] = mod
b.builds["node-tools "+old] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}}
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest}
b.builds["node-tools "+old] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest,
Artifacts: []builds.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}}
b.builds["node-tools "+next] = &builds.BuilderBuild{State: "building", Manifest: mod.Manifest}
if row.done {
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}}
b.builds["node-tools "+next] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest,
Artifacts: []builds.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}}
}
fps["node-tools"] = row.want
out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1", Running: running}, fps)
@@ -253,12 +255,12 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
name = "runtime"
}
mod := b.modules[m]
mod.FP2 = fps[m]
mod.Fp2 = fps[m]
b.modules[m] = mod
b.builds[m+" "+fps[m]] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
Artifacts: []delivery.BuiltArtifact{{Name: name, Kind: "bundle", Reference: "artifact-store://" + m + "/" + name + "/blobs/" + dg, Digest: dg}}}
b.builds[m+" "+fps[m]] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest,
Artifacts: []builds.BuiltArtifact{{Name: name, Kind: "bundle", Reference: "artifact-store://" + m + "/" + name + "/blobs/" + dg, Digest: dg}}}
if m == "node-tools" && row.building {
b.builds[m+" "+fps[m]] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest}
b.builds[m+" "+fps[m]] = &builds.BuilderBuild{State: "building", Manifest: mod.Manifest}
}
}
out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1"}, fps)
+25 -20
View File
@@ -14,20 +14,25 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
"strings"
"time"
)
// The states a module names in its manifest's `reads` to compose through StatesReader.
const (
PlacementRead = delivery.PlacementRead
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
BuildsRead = delivery.BuilderModule + ".builds"
// The holders' names are kept here, in the engine's own code, as the names of the states a manifest's reads use
// (ADR 0634 §1, finding F4); a seat's contract names a state locally, not the module that holds it.
PlacementRead = meshmoduleassignerclient.PlacementRead
ModulesRead = "mesh-delivery-indexer.modules"
ManifestsRead = "mesh-delivery-indexer.manifests"
BuildsRead = "mesh-delivery-builder.builds"
HaveRead = "node-engine.have"
ResolvedRead = "mesh-config.resolved"
)
@@ -78,8 +83,8 @@ func (r statesReader) each(read string, visit func(key string, value []byte)) er
return nil
}
func (r statesReader) Placements(context.Context) (map[string]delivery.Placement, error) {
keys := delivery.NewPlacementKeys()
func (r statesReader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
keys := meshmoduleassignerclient.NewKeys()
err := r.each(PlacementRead, func(k string, v []byte) {
_, _ = keys.Put(k, v, 0, false) // a key that does not read places nothing, as before
})
@@ -98,11 +103,11 @@ func (r statesReader) Records(context.Context) (map[string]nodeengine.NodeRecord
return out, err
}
func (r statesReader) Modules(context.Context) (map[string]delivery.Module, error) {
out := map[string]delivery.Module{}
func (r statesReader) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) {
out := map[string]meshcatalog.ModulesByModuleValue{}
err := r.each(ModulesRead, func(k string, v []byte) {
if module, err := delivery.ParseModulesKey(k); err == nil {
if m, err := delivery.DecodeModule(v); err == nil {
if module, err := meshcatalogclient.ParseModulesKey(k); err == nil {
if m, err := meshcatalogclient.DecodeModule(v); err == nil {
out[module] = m
}
}
@@ -111,7 +116,7 @@ func (r statesReader) Modules(context.Context) (map[string]delivery.Module, erro
}
func (r statesReader) Manifest(_ context.Context, digest string) ([]byte, error) {
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return nil, err
}
@@ -122,11 +127,11 @@ func (r statesReader) Manifest(_ context.Context, digest string) ([]byte, error)
if !found {
return nil, fmt.Errorf("the indexer holds no manifest %s", digest)
}
m, err := delivery.DecodeManifest(v)
m, err := meshcatalogclient.DecodeManifest(v)
if err != nil {
return nil, err
}
return []byte(m.JSON), nil
return []byte(m.Manifest), nil
}
func (r statesReader) Values(_ context.Context, node, module string) (map[string]any, error) {
@@ -155,8 +160,8 @@ func (statesReader) Credentials(context.Context, string, []compose.CredentialKey
return nil, nil, nil
}
func (r statesReader) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) {
key, err := delivery.BuilderBuildKey(module, fp)
func (r statesReader) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) {
key, err := builds.BuilderBuildKey(module, fp)
if err != nil {
return nil, err
}
@@ -164,7 +169,7 @@ func (r statesReader) Build(_ context.Context, module, fp string) (*delivery.Bui
if err != nil || !found {
return nil, err
}
b, err := delivery.DecodeBuilderBuild(v)
b, err := builds.DecodeBuilderBuild(v)
if err != nil {
return nil, err
}
+5 -5
View File
@@ -12,11 +12,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// HeldPlans is one scope's plans, as PlansHeld answers them: err a refusal that is no single module's.
@@ -75,13 +75,13 @@ func PlansHeldTwice(ctx context.Context, r Reader, mesh Mesh, s Scope) (at, newe
// onceBuilds is r's Build, each module's build at a fingerprint read once: both scopes of one composition read the
// builds of the same moment.
func onceBuilds(r Reader) func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) {
func onceBuilds(r Reader) func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) {
type answer struct {
b *delivery.BuilderBuild
b *builds.BuilderBuild
err error
}
read := map[string]answer{}
return func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) {
return func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) {
key := module + " " + fp
if a, ok := read[key]; ok {
return a.b, a.err
@@ -101,7 +101,7 @@ type takenInKeeper interface {
// takenInKey is what a take-in is made from: the artifact store's address and the build's record (its manifest's digest, its artifacts by digest), read
// whole; an archive is checked against its digest on every read, so the same record takes in to the same bytes.
func takenInKey(module, fp, address string, b delivery.BuilderBuild) (string, bool) {
func takenInKey(module, fp, address string, b builds.BuilderBuild) (string, bool) {
raw, err := json.Marshal(b)
if err != nil {
return "", false