Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/isolate.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

111 lines
4.1 KiB
Go

package main
// The run steps shut off (novox/hq ADR 0448; issue 1309, lead decision L2 of R4e): the type-check, the package's test
// script, tsc and esbuild run the change's code, so each runs through mesh-sdk's buildstep.Isolated, in bubblewrap: no
// network, none of the account (its home, its ssh key, its registry secret, its bus login, the runtime directory), the
// workspace read-write, the toolchain read-only, the shared npm cache read-only beneath a throwaway writable layer, or
// read-write for a trunk build alone (publish). The fetch step (git, npm ci of the module's own packages) holds the key
// and the network and runs none of the change's code, so it runs as before.
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// BubblewrapLeast is the oldest bubblewrap a run step is shut off with: --overlay-src and --tmp-overlay, which keep the
// shared cache unchanged by a candidate, came in 0.10.
const BubblewrapLeast = "0.10"
// npmCache is the shared npm cache a package's test script reads.
func (m *Module) npmCache() string { return filepath.Join(m.Toolchain.Paths.Cache, "npm") }
// isolation is how a run step in the workspace root is shut off.
func (m *Module) isolation(root string, write bool) buildstep.Isolation {
return buildstep.Isolation{
Workspace: root,
ReadOnly: []string{m.toolchainDir()},
Caches: []string{m.npmCache()},
WriteCaches: write,
Env: []string{"PATH=" + pathOrDefault(), "LC_ALL=C"},
}
}
// stepRunner is the runner of a run step in the workspace root: buildstep.Isolated over m.Run, every command shut off;
// write is a trunk build's alone.
func (m *Module) stepRunner(root string, write bool) (forgekey.Runner, error) {
// bubblewrap binds only a directory that is there.
if err := os.MkdirAll(m.npmCache(), 0o700); err != nil {
return nil, fmt.Errorf("the shared cache %s: %w", m.npmCache(), err)
}
return buildstep.Isolated(m.Run, m.isolation(root, write)), nil
}
// ProbeBubblewrap reads the installed bubblewrap's version (`bwrap --version`: "bubblewrap 0.13.0").
func ProbeBubblewrap(ctx context.Context) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
r := forgekey.ExecRunnerClean(0)(ctx, []string{"PATH=" + defaultPath}, buildstep.Bubblewrap, "--version")
if r.Status != 0 || r.Err != "" {
return "", fmt.Errorf("`%s --version` failed: %s", buildstep.Bubblewrap, firstLine(r.Err+"\n"+r.Stderr))
}
return parseBubblewrapVersion(r.Stdout)
}
var bubblewrapVersion = regexp.MustCompile(`^bubblewrap (\d+)\.(\d+)(\.\d+)?\s*$`)
func parseBubblewrapVersion(out string) (string, error) {
f := bubblewrapVersion.FindStringSubmatch(strings.TrimSpace(out))
if f == nil {
return "", fmt.Errorf("`%s --version` answered %q", buildstep.Bubblewrap, strings.TrimSpace(out))
}
return f[1] + "." + f[2] + f[3], nil
}
// bubblewrapProblem says why the installed bubblewrap cannot shut a run step off, or "".
func bubblewrapProblem(version string, err error) string {
if err != nil {
return "bubblewrap cannot be run, and every build and test step runs in it (ADR 0448): " + err.Error()
}
if !atLeast(version, BubblewrapLeast) {
return fmt.Sprintf("bubblewrap %s installed, and a build or test step needs %s or later (ADR 0448)", version, BubblewrapLeast)
}
return ""
}
// atLeast says whether the dotted version v is least or later.
func atLeast(v, least string) bool {
a, b := strings.Split(v, "."), strings.Split(least, ".")
for i := range b {
var x int
if i < len(a) {
x, _ = strconv.Atoi(a[i])
}
y, _ := strconv.Atoi(b[i])
if x != y {
return x > y
}
}
return true
}
// checkBubblewrap refuses a step when bubblewrap cannot shut it off; nil Bubblewrap probes nothing.
func (m *Module) checkBubblewrap(ctx context.Context) error {
if m.Bubblewrap == nil {
return nil
}
v, err := m.Bubblewrap(ctx)
if why := bubblewrapProblem(v, err); why != "" {
return &buildstep.Refusal{Text: why, Want: BubblewrapLeast + " or later", Have: v}
}
return nil
}