Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/level.go
T

250 lines
9.4 KiB
Go

package main
// The integration level shut off (novox/hq ADR 0448, ADR 0470; issue 1309 S5): its store, its bus and its tests run in
// one bubblewrap of their own, so the tests reach the level's postgres (a socket in the run's directory) and its bus (a
// loopback port of the step's own network) and nothing of the machine. The process in it is this binary started again
// as `mesh-build-go level <spec>`: it runs the level as integrationHere does and prints its answer as one line of JSON.
// The tools bundle never runs a level in its own process, and a level never serves tools.
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// levelArg is the first argument that makes this binary a step's shut-off process: the integration level's, and, when
// the merge changes this module, the candidate's program running a digest or a level (ADR 0535 §1, candidate.go).
const levelArg = "level"
// The steps a shut-off process runs (levelSpec.Step); empty is the integration level, as a spec said before the runner.
const (
stepDigest = "digest"
stepUnit = LevelUnit
stepIntegration = LevelIntegration
)
// levelSpec is what the shut-off process is asked: the step, the workspace, the packages and the -run pattern, the
// level's bounds; for a digest, its level.
type levelSpec struct {
Step string `json:"step,omitempty"`
Level string `json:"level,omitempty"`
Work string `json:"work"`
Cache string `json:"cache"`
Workspace string `json:"workspace"`
Packages []string `json:"packages,omitempty"`
Run string `json:"run,omitempty"`
Setup time.Duration `json:"setup"`
Bound time.Duration `json:"bound"`
NodeNames []string `json:"node-names,omitempty"`
}
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
type levelAnswer struct {
Result *TestResult `json:"result,omitempty"`
Digest *DigestResult `json:"digest,omitempty"`
Error string `json:"error,omitempty"`
}
// executable is this program's path, bound read-only into the step: Executable when a test set it.
func (m *Module) executable() (string, error) {
if m.Executable != "" {
return m.Executable, nil
}
exe, err := os.Executable()
if err != nil {
return "", err
}
return filepath.EvalSymlinks(exe)
}
// integrationShutOff runs the integration level in one shut-off process of this program and reads its answer.
func (m *Module) integrationShutOff(ctx context.Context, workspace string, packages []string, pattern string) (*TestResult, error) {
exe, err := m.executable()
if err != nil {
return nil, fmt.Errorf("the integration level's program: %w", err)
}
b := m.integrationBounds()
a, err := m.shutOff(ctx, exe, levelSpec{Step: stepIntegration, Workspace: workspace, Packages: packages, Run: pattern, Setup: b.Setup, Bound: b.Run})
if err != nil {
return nil, err
}
if a.Result == nil {
return nil, fmt.Errorf("the integration level's shut-off process answered no result")
}
return a.Result, nil
}
// shutOff runs the program exe as one shut-off process of the workspace (`exe level <spec>`) and reads its answer: the
// workspace read-write, the program alone read-only, the shared caches beneath a throwaway layer, the Node build
// module's toolchains read-only for the integration level.
func (m *Module) shutOff(ctx context.Context, exe string, spec levelSpec) (levelAnswer, error) {
spec.Work, spec.Cache = m.Work, m.Cache
raw, err := json.Marshal(spec)
if err != nil {
return levelAnswer{}, err
}
for _, c := range m.caches() {
if err := os.MkdirAll(c, 0o700); err != nil {
return levelAnswer{}, fmt.Errorf("the shared cache %s: %w", c, err)
}
}
iso := m.isolation(spec.Workspace, false)
iso.ReadOnly = append(iso.ReadOnly, exe) // the program alone, not the directory it lies in
if spec.Step == stepIntegration || spec.Step == "" {
iso.ReadOnly = append(iso.ReadOnly, m.nodeToolchains()...)
}
r := buildstep.Isolated(m.Run, iso)(ctx, nil, exe, levelArg, string(raw))
var answer levelAnswer
line := lastLine(r.Stdout)
if err := json.Unmarshal([]byte(line), &answer); err != nil || (answer.Result == nil && answer.Digest == nil && answer.Error == "") {
return levelAnswer{}, fmt.Errorf("the %s step's shut-off process answered nothing it says (status %d %s): %s", stepName(spec.Step), r.Status, r.Err, firstLine(r.Stderr+"\n"+r.Stdout))
}
if answer.Error != "" {
return levelAnswer{}, fmt.Errorf("%s", answer.Error)
}
return answer, nil
}
// stepName names a spec's step in words.
func stepName(step string) string {
switch step {
case stepDigest:
return "digest"
case stepUnit:
return "unit level's"
}
return "integration level's"
}
// NodeToolchains is the Node build module's toolchains directory on this node: its cache directory (module.json
// resource `cache`, mode 0711) joined with toolchain, where it installs typescript, esbuild, @novox/mesh-sdk and nats
// from its own lock (ADR 0533 §1, §2; issue 1891).
const NodeToolchains = "/var/lib/mesh-build-node-cache/toolchain"
// nodeToolchains is what the integration level is given of the Node build module's toolchains: their directory,
// read-only, when it is there and this account may enter it, and nothing on a node where the Node build module has
// installed none (ADR 0533 §1). The unit level is never given it.
func (m *Module) nodeToolchains() []string {
dir := m.NodeToolchains
if dir == "" {
dir = NodeToolchains
}
if _, err := os.ReadDir(dir); err != nil {
return nil
}
return []string{dir}
}
// nodeToolchainMark is the file the Node build module writes in a toolchain directory once it is installed whole
// (mesh-build-node toolchain.go installedMark).
const nodeToolchainMark = ".installed"
// Has says whether this node has a need a check ask or a package's tests name (issue 890, ADR 0552; mesh-sdk
// delivery.AskNeeds), read from the node itself at every ask: node-build-node when the Node build module's toolchains
// directory, given to the integration level as nodeToolchains gives it, holds a toolchain installed whole (its mark and
// its package-lock.json readable by this account), and node, npm and bwrap are installed: what mesh-build-node's
// integration tests (realMachine) look for before they run. Any other need it lacks.
func (m *Module) Has(need string) bool {
if m.HasNeed != nil {
return m.HasNeed(need)
}
switch need {
case delivery.NeedNodeBuildNode:
dirs := m.nodeToolchains()
if len(dirs) == 0 {
return false
}
for _, p := range []string{"node", "npm", "bwrap"} {
if _, err := exec.LookPath(p); err != nil {
return false
}
}
entries, err := os.ReadDir(dirs[0])
if err != nil {
return false
}
for _, e := range entries {
dir := filepath.Join(dirs[0], e.Name())
if _, err := os.Stat(filepath.Join(dir, nodeToolchainMark)); err != nil {
continue
}
if f, err := os.Open(filepath.Join(dir, "package-lock.json")); err == nil {
f.Close()
return true
}
}
return false
}
return false
}
func lastLine(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
return strings.TrimSpace(lines[len(lines)-1])
}
// runLevelProcess is the shut-off process: args is the spec. It prints its answer and returns the exit status, 0 when it
// printed one.
func runLevelProcess(args []string) int {
say := func(a levelAnswer) int {
raw, _ := json.Marshal(a)
fmt.Println(string(raw))
return 0
}
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "usage: mesh-build-go level <spec>")
return 2
}
var spec levelSpec
if err := json.Unmarshal([]byte(args[0]), &spec); err != nil {
return say(levelAnswer{Error: "the level's spec: " + err.Error()})
}
if !workspaceOK.MatchString(spec.Workspace) {
return say(levelAnswer{Error: fmt.Sprintf("%q is not a workspace id", spec.Workspace)})
}
m := &Module{Inside: true, Recipe: Current(), Work: spec.Work, Cache: spec.Cache, Home: os.Getenv("HOME"), UnitBound: spec.Bound,
IntegrationBounds: Bounds{Setup: spec.Setup, Run: spec.Bound}, Say: func(string, ...any) {}}
m.Run = StepRunner(m.Home)
return say(m.answer(context.Background(), spec))
}
// answer runs the spec's step in this process, already shut off: a digest of a level, the unit level or the integration
// level of the workspace.
func (m *Module) answer(ctx context.Context, spec levelSpec) levelAnswer {
tree := filepath.Join(m.Work, spec.Workspace, "tree")
switch spec.Step {
case stepDigest:
d, err := m.digestHere(ctx, spec.Workspace, spec.Level, spec.NodeNames)
if err != nil {
return levelAnswer{Error: err.Error()}
}
return levelAnswer{Digest: &d}
case stepUnit:
res, err := m.runLevel(ctx, tree, levelOpts{workspace: spec.Workspace, level: LevelUnit, packages: spec.Packages, run: spec.Run,
bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace, spec.NodeNames)})
if err != nil {
return levelAnswer{Error: err.Error()}
}
return levelAnswer{Result: res}
case stepIntegration, "":
major, err := m.checkPostgres(ctx)
if err != nil {
return levelAnswer{Error: err.Error()}
}
res, err := m.integrationHere(ctx, spec.Workspace, tree, spec.Packages, spec.Run, major)
if err != nil {
return levelAnswer{Error: err.Error()}
}
return levelAnswer{Result: res}
}
return levelAnswer{Error: fmt.Sprintf("the step %q is not digest, unit or integration", spec.Step)}
}