Send each resource's module and each module's build to a node that reads them; pin mesh-host at 0b391f49a25f (issue 1145)

This commit is contained in:
2026-10-18 19:11:10 +02:00
parent fc43238267
commit dc65fe2319
15 changed files with 407 additions and 49 deletions
+87
View File
@@ -0,0 +1,87 @@
package main
import (
"encoding/json"
"fmt"
"testing"
"github.com/novox/mesh-controller/internal/chain"
"github.com/novox/mesh-host/validate"
)
// The module of each resource and the build of each module (novox/hq issue 1145, design 47 release 1-2). The
// composer's owner of an id and the module's build go on the wire only to a machine whose node-engine said it reads
// them: an older one decodes strictly and refuses a key it does not know, whole.
func modulesDeclaration(reads bool) sendable {
return sendable{
Resources: []map[string]any{
{"id": "web.app.conf", "type": "file", "path": "/etc/app.conf", "content": "x\n"},
{"id": "web.app.data", "type": "file", "path": "/etc/data", "content": "d\n"},
{"id": "mesh.guard", "type": "file", "path": "/etc/guard", "content": "g\n"}},
moduleOf: map[string]string{"web.app.conf": "web.app", "web.app.data": "web.app"},
buildIDs: map[string]string{"web.app": "build-7"},
ReadsModules: reads,
}
}
// 1145/AC1
func TestAMachineThatReadsModulesIsSentEachResourcesModuleAndEachModulesBuild(t *testing.T) {
keys := bodyKeys(t, modulesDeclaration(true))
builds, _ := keys["builds"].(map[string]any)
if len(builds) != 1 || builds["web.app"] != "build-7" {
t.Fatalf("1145/AC1: the body carries builds %v", keys["builds"])
}
want := map[string]any{"web.app.conf": "web.app", "web.app.data": "web.app", "mesh.guard": nil}
for _, r := range keys["resources"].([]any) {
r := r.(map[string]any)
if r["module"] != want[r["id"].(string)] {
t.Errorf("1145/AC1: resource %v carries module %v, want %v", r["id"], r["module"], want[r["id"].(string)])
}
}
}
// 1145/AC2: the body is byte for byte what it is today.
func TestAMachineThatDoesNotReadModulesIsSentTheBodyItWasSentBefore(t *testing.T) {
d := modulesDeclaration(false)
got, err := d.Body()
if err != nil {
t.Fatal(err)
}
before := sendable{Resources: d.Resources}
want, _ := before.Body()
if string(got) != string(want) {
t.Fatalf("1145/AC2: the body changed:\n%s\nwas\n%s", got, want)
}
}
// 1145/AC4: the node-engine's own validator, as the gate runs it, takes the two words.
func TestTheNodeEnginesValidatorTakesAModulePerResourceAndABuildPerModule(t *testing.T) {
raw, err := modulesDeclaration(true).Body()
if err != nil {
t.Fatal(err)
}
var generic map[string]any
if err := json.Unmarshal(raw, &generic); err != nil {
t.Fatal(err)
}
if problems := validate.Declaration(raw); len(problems) != 0 {
t.Fatalf("1145/AC4: refused: %q\n%s", problems, fmt.Sprint(generic))
}
}
// The build a module carries is the one its assignment names when that is the commit carried, else the one built from
// the commit carried; none when neither is known.
func TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt(t *testing.T) {
runs := map[string]chain.Ref{"a": {Build: "b-a", Commit: "c1"}, "b": {Build: "b-old", Commit: "c0"}}
looked := map[string]string{"b/c2": "b-new"}
got, err := wireBuilds(map[string]string{"a": "c1", "b": "c2", "c": "c3", "d": ""}, runs,
func(module, commit string) (string, error) { return looked[module+"/"+commit], nil })
if err != nil {
t.Fatal(err)
}
want := map[string]string{"a": "b-a", "b": "b-new"}
if len(got) != len(want) || got["a"] != want["a"] || got["b"] != want["b"] {
t.Fatalf("1145: builds %v, want %v", got, want)
}
}
+89 -22
View File
@@ -16,6 +16,7 @@ import (
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/chain"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/licences"
"github.com/novox/mesh-controller/internal/link"
@@ -500,33 +501,75 @@ func declarationWith(ctx context.Context, open *stores, node string,
// And which build of each module it carries, for the send to record (novox/hq issue 259, ADR
// 0221). Read only on the send path: a question about what would be sent records nothing.
if choosing == Allocating {
current, err := open.inventory.CurrentBuilds(ctx)
carried, err := carriedOn(ctx, open, node, plan, composed.LeftOut, true)
if err != nil {
return sendable{}, err
}
before, known, err := open.inventory.SentBuilds(ctx, node)
if err != nil {
return sendable{}, err
}
if !known {
before = nil
}
names := make([]string, 0, len(plan.Modules))
for _, m := range plan.Modules {
names = append(names, m.Module)
}
out.Builds = carriedBuilds(names, composed.LeftOut, current, before)
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0245).
kept, err := recordedKept(ctx, open, node)
if err != nil {
return sendable{}, err
}
for m, was := range kept {
if _, carried := out.Builds[m]; carried {
out.Builds[m] = was
out.Builds = carried
out.Bindings = boundToData(plan, composed.LeftOut)
}
// And the module of each resource and the build of each module, to a machine whose node-engine said it reads them
// (novox/hq issue 1145) and to no other; the same whatever the composition is for, so what a question about the
// send digests is what the send digests.
reads, err := open.inventory.ReadsModules(ctx, record.ID)
if err != nil {
return sendable{}, err
}
if reads {
carried := out.Builds
if choosing != Allocating {
if carried, err = carriedOn(ctx, open, node, plan, composed.LeftOut, false); err != nil {
return sendable{}, err
}
}
out.Bindings = boundToData(plan, composed.LeftOut)
runs, err := open.inventory.AssignmentRuns(ctx, node)
if err != nil {
return sendable{}, err
}
builds, err := wireBuilds(carried, runs, func(module, commit string) (string, error) {
return open.inventory.BuildOf(ctx, module, commit)
})
if err != nil {
return sendable{}, err
}
out.ReadsModules, out.moduleOf, out.buildIDs = true, composed.Owner, builds
}
return out, nil
}
// carriedOn is the build of each module a declaration for node carries (see carriedBuilds), the module kept at the
// build a machine runs included (ADR 0245). Only a send (strict) fails for a kept module that cannot be read: a question
// about it is answered without.
func carriedOn(ctx context.Context, open *stores, node string, plan catalogue.Resolution, leftOut map[string]string,
strict bool) (map[string]string, error) {
current, err := open.inventory.CurrentBuilds(ctx)
if err != nil {
return nil, err
}
before, known, err := open.inventory.SentBuilds(ctx, node)
if err != nil {
return nil, err
}
if !known {
before = nil
}
names := make([]string, 0, len(plan.Modules))
for _, m := range plan.Modules {
names = append(names, m.Module)
}
out := carriedBuilds(names, leftOut, current, before)
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0245).
kept, err := recordedKept(ctx, open, node)
if err != nil {
if strict {
return nil, err
}
kept = nil
}
for m, was := range kept {
if _, carried := out[m]; carried {
out[m] = was
}
}
return out, nil
}
@@ -2109,3 +2152,27 @@ func sortedNames[V any](m map[string]V) []string {
sort.Strings(out)
return out
}
// wireBuilds is the build of each carried module that goes on the wire (novox/hq issue 1145): the build its assignment
// names when that assignment is of the commit carried, else the build made from that commit; a module with neither
// is not named.
func wireBuilds(carried map[string]string, runs map[string]chain.Ref, lookup func(module, commit string) (string, error)) (map[string]string, error) {
out := map[string]string{}
for module, commit := range carried {
if commit == "" {
continue
}
if ref, ok := runs[module]; ok && ref.Commit == commit && ref.Build != "" {
out[module] = ref.Build
continue
}
build, err := lookup(module, commit)
if err != nil {
return nil, err
}
if build != "" {
out[module] = build
}
}
return out, nil
}
+30
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
@@ -38,6 +39,13 @@ type sendable struct {
// the send; never on the wire.
composedFrom int64
actingEpoch uint64
// ReadsModules says the machine's node-engine reads a module per resource and a build per module (novox/hq issue
// 1145): only then are they sent, because an older node-engine refuses a key it does not know, whole.
ReadsModules bool
// moduleOf is the composer's owner of each resource id, and buildIDs the build each module carries (its id):
// kept beside the resources, and on the wire only to a machine that reads them.
moduleOf map[string]string
buildIDs map[string]string
// modules are the modules this declaration names, for the record of the send; never on the wire.
modules []string
// localNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR
@@ -111,6 +119,28 @@ type adoptionEnvelope struct {
// Body is the declaration's bytes, as sent and as digested.
func (s sendable) Body() ([]byte, error) {
envelope := map[string]any{"declaration": 1, "resources": s.Resources}
if s.ReadsModules {
// The module of each resource the composer owns (what the mesh adds of its own has none), and the build of each
// module (novox/hq issue 1145): to a machine that said it reads them, and no other.
named := make([]map[string]any, len(s.Resources))
for n, r := range s.Resources {
module, owned := s.moduleOf[fmt.Sprint(r["id"])]
if !owned {
named[n] = r
continue
}
with := make(map[string]any, len(r)+1)
for k, v := range r {
with[k] = v
}
with["module"] = module
named[n] = with
}
envelope["resources"] = named
if len(s.buildIDs) > 0 {
envelope["builds"] = s.buildIDs
}
}
if s.Adoption != nil {
envelope["adoption"] = s.Adoption
}