Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/isolate_test.go
T
jschoubben 56f842c3af mesh-build-node: run the type-check, the test script, tsc and esbuild in bubblewrap, so a change's code reaches no key, login or network (issue 1309, S1 S7 S8 S9, ADR 0448)
Each run step goes through mesh-sdk's buildstep.Isolated: no network, none of the account,
the workspace read-write, the toolchain read-only, the npm cache beneath a throwaway layer
unless the build step is told publish (a trunk build). The module declares the bubblewrap
package and refuses a step, unhealthy, while bwrap is missing or older than 0.10. The proof
test runs a probe script through the real test step.
2026-10-22 18:08:12 +02:00

179 lines
6.5 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"slices"
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// shutOff is one command a step ran in bubblewrap: what it bound read-write, read-only and beneath a throwaway layer,
// its bubblewrap flags, and the command.
type shutOff struct {
flags []string
binds, roBinds, overlays []string
env []string
command string
}
// bwrapArity is how many words each bubblewrap option the SDK writes takes.
var bwrapArity = map[string]int{"--unshare-all": 0, "--die-with-parent": 0, "--new-session": 0, "--clearenv": 0,
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1,
"--ro-bind": 2, "--ro-bind-try": 2, "--bind": 2, "--symlink": 2, "--setenv": 2}
// throughBubblewrap reads a bubblewrap command back (recorded in seen) and hands its command to run with the environment
// bubblewrap sets (a name set twice keeps its last value, as bubblewrap's --setenv does); any other command goes to run
// as it is.
func throughBubblewrap(t *testing.T, run forgekey.Runner, seen *[]shutOff) forgekey.Runner {
t.Helper()
var mu sync.Mutex
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
if name != buildstep.Bubblewrap {
return run(ctx, env, name, args...)
}
var s shutOff
var names []string
values := map[string]string{}
i := 0
for ; i < len(args) && args[i] != "--"; i++ {
n, ok := bwrapArity[args[i]]
if !ok || i+n >= len(args) {
t.Errorf("bubblewrap was given %q, which the test does not know: %v", args[i], args)
return forgekey.Ran{Status: 1, Err: "unknown bubblewrap option"}
}
s.flags = append(s.flags, args[i])
switch args[i] {
case "--bind":
s.binds = append(s.binds, args[i+1])
case "--ro-bind", "--ro-bind-try":
s.roBinds = append(s.roBinds, args[i+1])
case "--overlay-src":
s.overlays = append(s.overlays, args[i+1])
case "--setenv":
if _, set := values[args[i+1]]; !set {
names = append(names, args[i+1])
}
values[args[i+1]] = args[i+2]
}
i += n
}
if i+1 >= len(args) {
t.Errorf("bubblewrap was given no command: %v", args)
return forgekey.Ran{Status: 1, Err: "no command"}
}
for _, n := range names {
s.env = append(s.env, n+"="+values[n])
}
s.command = args[i+1] + " " + strings.Join(args[i+2:], " ")
mu.Lock()
*seen = append(*seen, s)
mu.Unlock()
return run(ctx, s.env, args[i+1], args[i+2:]...)
}
}
// checkShutOff fails the test unless s ran with no network and none of the account, the toolchain read-only, and the
// shared cache beneath a throwaway layer unless write.
func checkShutOff(t *testing.T, m *Module, root string, s shutOff, write bool) {
t.Helper()
for _, f := range []string{"--unshare-all", "--clearenv", "--die-with-parent"} {
if !slices.Contains(s.flags, f) {
t.Errorf("%s ran without %s", s.command, f)
}
}
home := m.Toolchain.Paths.Home
for _, b := range append(append([]string{}, s.binds...), s.roBinds...) {
if b == home || strings.HasPrefix(b, home+"/") || strings.HasPrefix(b, "/run") {
t.Errorf("%s was given %s: the account's home or the runtime directory", s.command, b)
}
}
for _, e := range s.env {
if e == "HOME="+home {
t.Errorf("%s was told the account's home", s.command)
}
}
if !slices.Contains(s.roBinds, m.toolchainDir()) || slices.Contains(s.binds, m.toolchainDir()) {
t.Errorf("%s was not given the toolchain read-only: %v %v", s.command, s.roBinds, s.binds)
}
c := m.npmCache()
bound, layered := slices.Contains(s.binds, c), slices.Contains(s.overlays, c)
if write != bound || write == layered {
t.Errorf("%s: the shared cache bound %v, layered %v; a trunk build (%v) alone writes it", s.command, bound, layered, write)
}
if !slices.Contains(s.binds, root) {
t.Errorf("%s was not given its workspace %s read-write: %v", s.command, root, s.binds)
}
}
// 1309-S7: the type-check and the test script run shut off, the cache beneath a throwaway layer.
func TestTheTestStepRunsItsCommandsShutOff(t *testing.T) {
m, f, work := newMachine(t)
workspaceWith(t, work, map[string]string{"index.ts": `export const a = 1;`, "package.json": `{"scripts":{"test":"x"}}`})
if _, err := m.test(context.Background(), testArgs("unit")); err != nil {
t.Fatal(err)
}
if len(f.shut) != 2 || len(f.calls) != 2 {
t.Fatalf("shut off %d of %d commands, want the type-check and the script: %v", len(f.shut), len(f.calls), f.calls)
}
for _, s := range f.shut {
checkShutOff(t, m, work+"/w1", s, false)
}
}
// 1309-S8: tsc and esbuild of a candidate's build run shut off with the cache beneath a throwaway layer; a trunk build
// (publish) writes it.
func TestTheBuildStepRunsShutOffAndOnlyATrunkBuildWritesTheCache(t *testing.T) {
for _, publish := range []bool{false, true} {
m, f, work := builtMachine(t, "")
ws := "w1"
args := buildArgs()
if publish {
args["publish"] = true
}
if _, err := m.build(context.Background(), args); err != nil {
t.Fatalf("publish %v: %v", publish, err)
}
if len(f.shut) == 0 || len(f.shut) != len(f.calls) {
t.Fatalf("publish %v: shut off %d of %d commands", publish, len(f.shut), len(f.calls))
}
for _, s := range f.shut {
checkShutOff(t, m, work+"/"+ws, s, publish)
}
}
}
// 1309-S1: a step is refused, and the module is unhealthy, while bubblewrap is missing or older than 0.10.
func TestAnOldOrMissingBubblewrapRefusesTheStepsAndIsUnhealthy(t *testing.T) {
for _, c := range []struct {
version string
err error
ok bool
}{{"0.13.0", nil, true}, {"0.10", nil, true}, {"0.9.0", nil, false}, {"", errors.New("bwrap: not found"), false}} {
m, _, _ := newMachine(t)
m.Probe = func(context.Context) (string, error) { return "v22.20.0", nil }
m.Toolchains = func(string, any) (uint64, error) { return 1, nil }
m.Bubblewrap = func(context.Context) (string, error) { return c.version, c.err }
err := m.Check(context.Background())
raw, _ := json.Marshal(m.Health(context.Background()))
if c.ok {
if err != nil || strings.Contains(string(raw), "unhealthy") {
t.Errorf("bubblewrap %q: refused %v, health %s", c.version, err, raw)
}
continue
}
var ref *buildstep.Refusal
if !errors.As(err, &ref) || !strings.Contains(err.Error(), "bubblewrap") {
t.Errorf("bubblewrap %q %v: Check answered %v", c.version, c.err, err)
}
if !strings.Contains(string(raw), "unhealthy") || !strings.Contains(string(raw), "bubblewrap") {
t.Errorf("bubblewrap %q %v: health %s", c.version, c.err, raw)
}
}
}