From 03b7fcd7bb21e9e62e8aac8a34d8b017b08019fa Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 10:59:55 +0100 Subject: [PATCH] The sandbox fails a node whose change refuses a module the node-engine did not hold, in that module's words, instead of passing it with the rest composed (issue 774, review of the sandbox input) --- internal/compose/frombus/frombus.go | 16 +++++++++++++++- internal/composer/composer_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 164baf2..c17e0f2 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -604,7 +604,21 @@ func ComposeSent(in compose.Input, node string) (map[string]delivery.Composed, b held = map[string]string{} } out, _, _, err := composeEach(in, node, c.Builds, false, c.Running, held, nil) - return out, true, err + if err != nil { + return nil, true, err + } + // A module the sandbox held that the node-engine did not is the change refusing it: the node does not compose, said + // in that module's words, never passed with the rest composed (issue 774, review of the sandbox input). + var refused []string + for _, m := range sortedKeys(held) { + if _, was := c.Held[m]; !was { + refused = append(refused, m+": "+held[m]) + } + } + if len(refused) > 0 { + return nil, true, fmt.Errorf("on %s the change refuses %s", node, strings.Join(refused, "; ")) + } + return out, true, nil } // Plans is the provisioning plan of each module handed to the provisioner on the scope's node, composed from the bus diff --git a/internal/composer/composer_test.go b/internal/composer/composer_test.go index 991b0ba..7ae1e90 100644 --- a/internal/composer/composer_test.go +++ b/internal/composer/composer_test.go @@ -143,6 +143,30 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { 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) } + // A module the change refuses beside the folded one fails the node in its words, never passed with the rest + // composed (review of the sandbox input); held already where the node-engine composed, it stays held as there. + app, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", ` + + `"resources": [{"id": "env", "type": "file", "path": "/var/lib/app/env", "mode": "0600", "content": "N=${secret:repository}\n"}]}`)) + if err != nil { + t.Fatal(err) + } + in.Catalogue["app"] = app + in.Nodes[0].Assigned = []string{"bus", "app"} + refused, err := Run(request(in)) + if err != nil { + t.Fatal(err) + } + if m := refused.Cases[0].Machines["a"]; m.Composes || !strings.Contains(m.Reason, "app: ") || !strings.Contains(m.Reason, "repository") { + t.Errorf("a module the change refuses on a node composed as its node-engine does: %+v", m) + } + in.ComposedHow.Held = map[string]string{"app": "held on the node"} + held, err := Run(request(in)) + if err != nil { + t.Fatal(err) + } + if m := held.Cases[0].Machines["a"]; !m.Composes { + t.Errorf("a module the node-engine holds too: %+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 == "" {