Each run step goes through mesh-sdk's buildstep.Isolated: no network, none of the account, the workspace read-write, the Go caches beneath a throwaway layer unless the build step is told publish (a trunk build). The integration level runs its store, its bus and its tests in one shut-off process (this program again, level <spec>), so they reach each other and nothing else. build_go_test takes a -run pattern (a try's tests). 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 test through the real unit level.
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package main
|
|
|
|
// The run steps shut off (novox/hq ADR 0448; issue 1309, lead decision L2 of R4e): every command that runs a change's
|
|
// code (go test, go build, go list) runs through mesh-sdk's buildstep.Isolated, in bubblewrap: no network, none of the
|
|
// account (its home, its ssh key, its bus login, the runtime directory), the workspace read-write, the shared Go caches
|
|
// read-only under a throwaway writable layer, or read-write for a trunk build alone (publish). The fetch step (git, go
|
|
// mod download) 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"
|
|
|
|
"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 caches unchanged by a candidate, came in 0.10.
|
|
const BubblewrapLeast = "0.10"
|
|
|
|
// caches are the shared warm caches of the module: Go's module cache and its build cache.
|
|
func (m *Module) caches() []string {
|
|
return []string{filepath.Join(m.Cache, "gomod"), filepath.Join(m.Cache, "gobuild")}
|
|
}
|
|
|
|
// isolation is how a run step of the workspace is shut off: the workspace (its tree, its siblings, its run directories,
|
|
// its packed artifacts) read-write, the caches beneath a throwaway layer unless write, and an environment of a PATH and
|
|
// a locale alone (the step's HOME is its own, inside its /tmp).
|
|
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
|
|
path := os.Getenv("PATH")
|
|
if path == "" {
|
|
path = defaultPath
|
|
}
|
|
return buildstep.Isolation{
|
|
Workspace: filepath.Join(m.Work, workspace),
|
|
Caches: m.caches(),
|
|
WriteCaches: write,
|
|
Env: []string{"PATH=" + path, "LC_ALL=C"},
|
|
}
|
|
}
|
|
|
|
// stepRunner is the runner of a run step of the workspace: buildstep.Isolated over m.Run, so every command it runs is
|
|
// shut off; write is true for a trunk build alone (the shared caches are written by trunk builds only). Inside is the
|
|
// integration level's own process, already running shut off (level.go): it runs its commands as they are.
|
|
func (m *Module) stepRunner(workspace string, write bool) (forgekey.Runner, error) {
|
|
if m.Inside {
|
|
return m.Run, nil
|
|
}
|
|
for _, c := range m.caches() {
|
|
// bubblewrap binds only a directory that is there; the fetch makes them, a step on a fresh node may come first.
|
|
if err := os.MkdirAll(c, 0o700); err != nil {
|
|
return nil, fmt.Errorf("the shared cache %s: %w", c, err)
|
|
}
|
|
}
|
|
return buildstep.Isolated(m.Run, m.isolation(workspace, 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, ProbeTimeout)
|
|
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 "" when it can: it is missing, or
|
|
// older than BubblewrapLeast.
|
|
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 := 0; i < len(b); i++ {
|
|
var x, y 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 (a module that only
|
|
// guards).
|
|
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
|
|
}
|