diff --git a/internal/composer/composer.go b/internal/composer/composer.go index f32a233..9204bc1 100644 --- a/internal/composer/composer.go +++ b/internal/composer/composer.go @@ -144,10 +144,18 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) { } shelf[name] = m } + // A module the input carries as its node-engine took it in from its build (SentInput) is composed so: the bus + // reader composes it from its build, never from the indexer's manifest a case gives. + for name, m := range base.Catalogue { + shelf[name] = m + } in.Catalogue = shelf in.Nodes = slices.Clone(in.Nodes) in.Holdings = slices.Clone(in.Holdings) for i := range in.Nodes { + if len(c.Kept[in.Nodes[i].Name]) == 0 { + continue // what the input keeps of this node, as its engine took it in, stands + } in.Nodes[i].Kept = nil if kept := c.Kept[in.Nodes[i].Name]; len(kept) > 0 { k, err := parse(kept, nil, nil) @@ -244,3 +252,38 @@ const Package = "github.com/novox/mesh-host/cmd/mesh-compose" // Decode reads a request (sdk.Decode); it refuses one that is not JSON or of another version. func Decode(data []byte) (Request, error) { return sdk.Decode(data) } + +// SentInput is the composition input a node-engine answers for the sandbox (delivery.ComposeCandidateAsk.Input), from the +// input it composed node from: the one shape both read (issue 1126, checks of mesh-host #200). Every other node's modules +// are left to the cases (the indexer's manifests, as the bus reader reads them too); node's own placed modules stay as the +// bus reader took them in from their builds (their artifacts, the texts of their templates archive, what is handed to the +// provisioner), with what node keeps at another build, which no case can give: the sandbox has no build or archive to +// read. Without them the sandbox composed another node than the engine (restic's templates and artifacts unread, postgres's +// secret reader handed and the module not). +func SentInput(in compose.Input, node string) compose.Input { + own := map[string]bool{} + for _, n := range in.Nodes { + if n.Name == node { + for _, m := range n.Assigned { + own[m] = true + } + } + } + taken := in.Catalogue + in.Catalogue = nil + for name, m := range taken { + if own[name] { + if in.Catalogue == nil { + in.Catalogue = map[string]catalogue.Manifest{} + } + in.Catalogue[name] = m + } + } + in.Nodes = slices.Clone(in.Nodes) + for i := range in.Nodes { + if in.Nodes[i].Name != node { + in.Nodes[i].Kept = nil + } + } + return in +} diff --git a/internal/composer/composer_test.go b/internal/composer/composer_test.go index 0e3e7cf..281c310 100644 --- a/internal/composer/composer_test.go +++ b/internal/composer/composer_test.go @@ -1,14 +1,18 @@ package composer import ( + "context" "encoding/json" "os" + "reflect" "strings" "testing" "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/frombus" + "github.com/novox/mesh-host/internal/compose/frombus/frombustest" ) const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files"}]}` @@ -70,6 +74,40 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { if v, err := Run(provided); err != nil || !v.Cases[0].Machines["a"].Composes { t.Errorf("a node assigned the mesh's own module: %+v, %v", v, err) } + // Issue 1126 (checks of mesh-host #200, fix-a-bug-met-twice): the sandbox composes one node from the same inputs as + // the bus reader, through one path. A mesh whose restic is built (its files from an archive) and handed to the + // provisioner composes n1 through frombus, and the request a node-engine answers for it (the input as the engine + // sends it, the indexer's manifests in the case) declares the same paths for n1. + t.Run("the sandbox composes a node as the bus reader does", func(t *testing.T) { + b, err := frombustest.Provisioned() + if err != nil { + t.Fatal(err) + } + ctx, scope := context.Background(), frombus.Scope{Node: frombustest.Node} + read, err := frombus.Input(ctx, b, frombus.Mesh{}, scope) + if err != nil { + t.Fatal(err) + } + d, problems, err := compose.Compose(read, frombustest.Node) + if err != nil || len(problems) > 0 { + t.Fatalf("the bus reader's input composes n1: %v %v", err, problems) + } + want := pathsOf(d) + sent := SentInput(read, frombustest.Node) + cases := Case{Name: "change", Manifests: map[string]Manifest{}} + for name, m := range b.Module { + cases.Manifests[name] = Manifest{Raw: b.Manifests[m.Manifest]} + } + r := withInput(t, Request{V: Version, Machines: []string{frombustest.Node}, Declarations: true, Cases: []Case{cases}}, sent) + v, err := Run(r) + if err != nil { + t.Fatal(err) + } + got := v.Cases[0].Machines[frombustest.Node] + if !got.Composes || !reflect.DeepEqual(got.Declared, want) { + t.Errorf("the sandbox composed n1 %+v; the bus reader declared %+v", got, want) + } + }) v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}) if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" { t.Fatalf("a request with no machines: %+v, %v", v, err) diff --git a/internal/selfcompose/candidate.go b/internal/selfcompose/candidate.go index 491e43c..9d8b091 100644 --- a/internal/selfcompose/candidate.go +++ b/internal/selfcompose/candidate.go @@ -15,6 +15,7 @@ import ( "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/compose/frombus" + "github.com/novox/mesh-host/internal/composer" ) // **A check's candidate, composed in shadow on a call** (novox/hq ADR 0593 ยง7): the node seat's verb compose-candidate. @@ -37,6 +38,10 @@ var errNotReady = errors.New("this node's shadow has not read every input yet") // nothing at all. A node not done by then answers unready, naming what it waits on. var candidateWithin = delivery.ComposeAnswerBound - 10*time.Second +// maxSentInput is the most composition input an answer carries: the bus carries 1 MiB in one message (its max_payload), +// and the answer's own fields and envelope come beside it. +const maxSentInput = 900 << 10 + // warmAtOnce is how many of the indexer's manifests the shadow reads at once before it composes a candidate. const warmAtOnce = 8 @@ -184,15 +189,18 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery if err != nil { return } - // The request's input carries no catalogue (composer.Request.Input): the cases give the manifests. - in.Catalogue = nil - for i := range in.Nodes { - in.Nodes[i].Kept = nil - } - if body, err := json.Marshal(in); err == nil { + // As the sandbox reads it (composer.SentInput, the one shape): this node's own modules as taken in from their + // builds, every other module's manifest left to the cases. + if body, err := json.Marshal(composer.SentInput(in, laid.env.Node)); err == nil { a.Input = body } }) + if len(a.Input) > maxSentInput { + // Past what the bus carries in one answer, the checker would hear nothing: refused, saying so. + size := len(a.Input) + a.Input = nil + return refuse(fmt.Sprintf("the composition input this node composed from is %d bytes, more than the %d an answer on the bus carries", size, maxSentInput)) + } if ctx.Err() != nil { _, on := run.said(c.now()) return unready(fmt.Sprintf("the node did not compose the candidate within %s: it waits on %s", candidateWithin, on))