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.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user