250 lines
9.4 KiB
Go
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)}
|
|
}
|