The sandbox composes a node as its node-engine does, from an input that says how, and names a node it cannot judge from an older engine's input (issue 774, ADR 0593 §9)
This commit is contained in:
@@ -571,6 +571,42 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s
|
||||
return out, skipped, held, err
|
||||
}
|
||||
|
||||
// ComposingInput is the input the scope's node composes itself from at the builds fps, as Compose reads it (the same read,
|
||||
// the refusals it holds), with how Compose composes from it (compose.Input.ComposedHow): what a node-engine sends a
|
||||
// check's sandbox, so the sandbox composes the node through ComposeSent, the path Compose takes (issue 774). Read no
|
||||
// credential: the sandbox composes no pair value.
|
||||
func ComposingInput(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (compose.Input, error) {
|
||||
held := map[string]string{}
|
||||
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, fps)
|
||||
if err != nil {
|
||||
return compose.Input{}, err
|
||||
}
|
||||
c := &compose.ComposedHow{Node: s.Node, Builds: maps.Clone(fps), Held: held}
|
||||
for _, run := range s.Running {
|
||||
if _, isHeld := held[run.Module]; isHeld && run.Module != "" {
|
||||
c.Running = append(c.Running, run)
|
||||
}
|
||||
}
|
||||
in.ComposedHow = c
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// ComposeSent composes node from an input ComposingInput read, through the path Compose takes (composeEach, the read's holds
|
||||
// first): what a check's sandbox runs. False when the input says no way of composing node (an input from a node-engine
|
||||
// before ComposedHow): the caller cannot compose it as the node does.
|
||||
func ComposeSent(in compose.Input, node string) (map[string]delivery.Composed, bool, error) {
|
||||
c := in.ComposedHow
|
||||
if c == nil || c.Node != node {
|
||||
return nil, false, nil
|
||||
}
|
||||
held := maps.Clone(c.Held)
|
||||
if held == nil {
|
||||
held = map[string]string{}
|
||||
}
|
||||
out, _, _, err := composeEach(in, node, c.Builds, false, c.Running, held, nil)
|
||||
return out, true, err
|
||||
}
|
||||
|
||||
// Plans is the provisioning plan of each module handed to the provisioner on the scope's node, composed from the bus
|
||||
// alone (novox/hq ADR 0570 clause 2): the controller's handedPlans over the same input (mesh-control
|
||||
// cmd/mesh-controller/compose_sdk.go), its rendering in the Foreseeing mode with the mesh's generators. A plan names
|
||||
|
||||
@@ -73,6 +73,13 @@ type Input struct {
|
||||
// issue 1126, C6). Used only while OperatorExport is nil.
|
||||
KeptRuns map[string]map[string]map[string]any
|
||||
|
||||
// ComposedHow is how the node-engine that read this input composes its node from it (frombus.Compose): the builds
|
||||
// it composes each module at, the modules it holds and what runs of each held one. A node-engine answers it with the
|
||||
// input it sends a check's sandbox (composer.SentInput), so the sandbox composes the node through the same path, a
|
||||
// module folded in the node's own composition kept as it runs rather than resolved from its manifest (issue 774, the
|
||||
// check of the bus want: novox's nats). Nil from every other reader and from a node-engine before it.
|
||||
ComposedHow *ComposedHow
|
||||
|
||||
// Issued is the credentials made for the consumers of each provider (inv.SecretsFrom): one row for each pair credential
|
||||
// that exists, by the provider node it is for. The grants a provider must create are read from these, so a credential not
|
||||
// made yet has no grant until the send that makes it. An assigner fills it from the facts snapshot's Grants of each
|
||||
@@ -231,6 +238,19 @@ type Node struct {
|
||||
RunsTools map[string][]catalogue.Bundle
|
||||
}
|
||||
|
||||
// ComposedHow is how a node-engine composes its node from an Input (Input.ComposedHow).
|
||||
type ComposedHow struct {
|
||||
// Node is the node composed.
|
||||
Node string
|
||||
// Builds is the build each module is composed at, by module: a module the node runs folded in its own composition
|
||||
// at a build of the old form has none, and is composed there as it runs (Node.InOwn, Node.RunsOwn).
|
||||
Builds map[string]string
|
||||
// Held is each module the read held, with why: composed as what runs of it, and the rest without it.
|
||||
Held map[string]string
|
||||
// Running is what runs of each held module, its own composition, by which it is carried as it runs.
|
||||
Running []delivery.Composed
|
||||
}
|
||||
|
||||
// Place is where a node is, as its placement says (inv.Overlay).
|
||||
type Place struct {
|
||||
Key string
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/composer"
|
||||
@@ -205,8 +206,25 @@ func machine(in compose.Input, node, depth string, declarations bool) Machine {
|
||||
}
|
||||
return Machine{Composes: true}
|
||||
}
|
||||
// Composed as the node-engine composes it, when its input says how (compose.Input.ComposedHow, issue 774): the same
|
||||
// path, at the same builds, a module folded in its own composition kept as it runs.
|
||||
if !declarations {
|
||||
if _, how, err := frombus.ComposeSent(in, node); how {
|
||||
if err != nil {
|
||||
return Machine{Reason: err.Error()}
|
||||
}
|
||||
return Machine{Composes: true}
|
||||
}
|
||||
}
|
||||
d, problems, err := compose.Compose(in, node)
|
||||
if err != nil {
|
||||
// An input from a node-engine before ComposedHow says nothing of what runs of the modules folded in the node's
|
||||
// own composition: the sandbox cannot compose them as the node does, so it does not judge the node, and names
|
||||
// it (ADR 0593 §9), never a failure of the change and never a silent pass.
|
||||
if folded := foldedUnsaid(in, node); len(folded) > 0 && in.ComposedHow == nil {
|
||||
return Machine{Composes: true, Reason: NotJudged + strings.Join(folded, ", ") +
|
||||
" (its own composition carries them as they run, and the sandbox cannot): " + err.Error()}
|
||||
}
|
||||
return Machine{Reason: err.Error()}
|
||||
}
|
||||
m := Machine{Composes: len(problems) == 0, Problems: problems, Refusals: d.Refusals()}
|
||||
@@ -219,6 +237,32 @@ func machine(in compose.Input, node, depth string, declarations bool) Machine {
|
||||
return m
|
||||
}
|
||||
|
||||
// NotJudged opens the reason of a machine the sandbox does not judge: its node-engine sent an input that does not say
|
||||
// how it composes (compose.Input.ComposedHow), and the node runs modules folded in its own composition.
|
||||
const NotJudged = "not judged: its node-engine sends no running state for "
|
||||
|
||||
// foldedUnsaid is each module node runs folded in its own composition (Node.InOwn) whose manifest names a build: what
|
||||
// the node composes as it runs, and an input without ComposedHow cannot.
|
||||
func foldedUnsaid(in compose.Input, node string) []string {
|
||||
var out []string
|
||||
for _, n := range in.Nodes {
|
||||
if n.Name != node {
|
||||
continue
|
||||
}
|
||||
for m, ok := range n.InOwn {
|
||||
man, known := n.Kept[m]
|
||||
if !known {
|
||||
man, known = in.Catalogue[m]
|
||||
}
|
||||
if ok && known && compose.NamesBuild(man) {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// pathsOf is every resource of a declaration that names a path, with its owner, group and mode as declared, sorted by
|
||||
// path.
|
||||
func pathsOf(d compose.Declared) []Path {
|
||||
|
||||
@@ -108,6 +108,42 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
|
||||
t.Errorf("the sandbox composed n1 %+v; the bus reader declared %+v", got, want)
|
||||
}
|
||||
})
|
||||
// Issue 774 (the check of the bus want, the third of a kind: the sandbox's input differs from what the bus reader
|
||||
// composes): a node like the bus node, its bus module folded in its own composition at a build of the old form and no
|
||||
// build published for the module's server artifact, composes in the sandbox as its node-engine composes it, from the
|
||||
// input that says how (compose.Input.ComposedHow); from an older engine's input, which does not, it is named not
|
||||
// judged, never failed and never passed in silence.
|
||||
t.Run("a module folded in the node's own composition", func(t *testing.T) {
|
||||
bus := `{"module": "bus", "version": "1", "claims": [{"name": "node-files"}], ` +
|
||||
`"resources": [{"id": "server", "type": "container", "name": "bus-server", "artifact": "server"}], ` +
|
||||
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
|
||||
man, err := catalogue.Decode([]byte(bus))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true},
|
||||
RunsOwn: &delivery.Composed{Node: "a"}}
|
||||
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"bus": man}, Nodes: []compose.Node{node}}
|
||||
request := func(in compose.Input) Request {
|
||||
return withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
|
||||
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{}}}}, in)
|
||||
}
|
||||
older, err := Run(request(in))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m := older.Cases[0].Machines["a"]; !m.Composes || !strings.HasPrefix(m.Reason, NotJudged+"bus") {
|
||||
t.Errorf("an older engine's input of a node with a folded module: %+v", m)
|
||||
}
|
||||
in.ComposedHow = &compose.ComposedHow{Node: "a", Builds: map[string]string{}}
|
||||
sent, err := Run(request(in))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m := sent.Cases[0].Machines["a"]; !m.Composes || m.Reason != "" {
|
||||
t.Errorf("the input that says how the node composes, its bus folded: %+v", m)
|
||||
}
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -181,11 +181,13 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
|
||||
if err != nil {
|
||||
return unready(err.Error())
|
||||
}
|
||||
laidComps, laidHeld, laidErr := laid.composeShadow(ctx, running, touched, func(scope frombus.Scope) {
|
||||
laidComps, laidHeld, laidErr := laid.composeShadow(ctx, running, touched, func(scope frombus.Scope, fps map[string]string) {
|
||||
if !ask.Input {
|
||||
return
|
||||
}
|
||||
in, err := frombus.Input(ctx, reader{c: laid}, laid.env.Mesh, scope)
|
||||
// Read as the composition reads it, at the builds it composes at, with how it composes (compose.Input.ComposedHow),
|
||||
// so the sandbox composes this node through the same path (issue 774).
|
||||
in, err := frombus.ComposingInput(ctx, reader{c: laid}, laid.env.Mesh, scope, fps)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -411,7 +413,7 @@ func (c *Composer) layCandidate(ctx context.Context, cand delivery.Candidate) (m
|
||||
// composeShadow composes this node from what c holds, as the shadow composes it (shadowBuilds): each module at the build
|
||||
// it runs, one placed and not run at the indexer's, and one the candidate laid (laid) at the candidate's. input, when
|
||||
// given, is handed the scope.
|
||||
func (c *Composer) composeShadow(ctx context.Context, running []delivery.Composed, laid map[string]bool, input func(frombus.Scope)) (map[string]delivery.Composed, map[string]string, error) {
|
||||
func (c *Composer) composeShadow(ctx context.Context, running []delivery.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]delivery.Composed, map[string]string, error) {
|
||||
node := c.env.Node
|
||||
c.mu.Lock()
|
||||
placement, placed := c.placements[node]
|
||||
@@ -444,7 +446,7 @@ func (c *Composer) composeShadow(ctx context.Context, running []delivery.Compose
|
||||
c.mu.Unlock()
|
||||
scope := frombus.Scope{Node: node, KeptAt: kept, InOwn: inOwn, Running: running, SameValue: c.sameValue}
|
||||
if input != nil {
|
||||
input(scope)
|
||||
input(scope, fps)
|
||||
}
|
||||
comps, _, held, err := frombus.Compose(ctx, reader{c: c}, c.env.Mesh, scope, fps)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package selfcompose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0593 §7: the verb compose-candidate composes a check's candidate on this node in shadow. A candidate that
|
||||
@@ -105,6 +107,14 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
||||
a.Own.Module != delivery.HaveNodeWord || a.Took > delivery.ComposeAnswerBound.Milliseconds() {
|
||||
t.Errorf("an accepted candidate answered %+v", a)
|
||||
}
|
||||
// Issue 774: the input it sends the sandbox says how it composed the node (the builds), so the sandbox composes it
|
||||
// through the same path.
|
||||
sent := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted, Input: true})
|
||||
var in compose.Input
|
||||
if err := json.Unmarshal(sent.Input, &in); err != nil || in.ComposedHow == nil || in.ComposedHow.Node != "n1" ||
|
||||
in.ComposedHow.Builds["lemurs"] != fpNew {
|
||||
t.Errorf("the input sent for the sandbox does not say how the node composes: %+v, %v", in.ComposedHow, err)
|
||||
}
|
||||
r := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: refusedKey})
|
||||
if r.Verdict != delivery.ComposeCandidateRefused || len(r.Modules) != 1 || r.Modules[0].State != delivery.ModuleRefusedState ||
|
||||
!strings.Contains(r.Modules[0].Words, "a field the judge refuses") {
|
||||
|
||||
Reference in New Issue
Block a user