Files
mesh-catalog/modules/mesh-build-image/cmd/mesh-build-image/integration_test.go
T
jschoubben 7c5d11e55d
mesh/delivery-group group feat/r4c9-image-build checking: 0 of 2 member(s) ready
mesh/merge-gate group feat/r4c9-image-build: its group's composed check has not given a verdict at the members' heads yet
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
Skip the real fetch test where buildah is not installed: every step is guarded by it
2026-10-22 10:43:07 +02:00

220 lines
9.1 KiB
Go

//go:build integration
package main
// The image build module against real git and real buildah (novox/hq ADR 0487 §1, §2, §6; issue 1703e). They start
// processes, so they carry the tag; the unit tests prove the commands with a fake runner. A test whose tools are not
// installed where the check runs skips, loudly: the mesh's checker has git but not buildah until buildah is delivered
// to its node, so the rootless isolation of §2 and §6 is proven live after the delivery (the issue says so).
import (
"context"
"encoding/json"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
func needTools(t *testing.T, what string, tools ...string) {
t.Helper()
for _, tool := range tools {
if _, err := exec.LookPath(tool); err != nil {
t.Skipf("NOT RUN: no %s installed where this check runs (%s); proven live on a build node", tool, what)
}
}
}
func git(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@example.invalid", "-c", "commit.gpgsign=false"}, args...)...)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
return strings.TrimSpace(string(out))
}
// realModule is a module over the real runner, a local directory standing in for the forge and the account running the test.
func realModule(t *testing.T) (*Module, *keptStates) {
t.Helper()
states := newStates()
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
home := t.TempDir()
m := &Module{Node: "ace", Account: u.Username, UID: u.Uid, Home: home, Work: t.TempDir(), Store: t.TempDir(),
Steps: states.putStep, Ctx: context.Background(), Say: t.Logf, Run: StepRunner(home),
FetchOptions: []buildstep.Option{buildstep.AllowFileRemote()}}
return m, states
}
// The fetch step on a real repository: the workspace holds the commit's tree, with the Dockerfile in it, and a base
// makes the merge preview.
func TestFetchOnARealRepository(t *testing.T) {
needTools(t, "the fetch step on a real repository (every step is guarded by buildah and a user namespace)", "git", "buildah", "newuidmap")
m, states := realModule(t)
root := t.TempDir()
bare, work := filepath.Join(root, "forge", "novox", "demo.git"), filepath.Join(root, "work")
for _, d := range []string{bare, work} {
os.MkdirAll(d, 0o755)
}
git(t, bare, "init", "-q", "--bare")
git(t, work, "init", "-q", "-b", "main")
os.MkdirAll(filepath.Join(work, "modules", "demo"), 0o755)
os.WriteFile(filepath.Join(work, "modules", "demo", "Dockerfile"), []byte("FROM scratch\n"), 0o644)
os.WriteFile(filepath.Join(work, "modules", "demo", "module.json"), []byte(`{"module":"demo","version":"1"}`), 0o644)
git(t, work, "add", ".")
git(t, work, "commit", "-q", "-m", "one")
git(t, work, "push", "-q", bare, "HEAD:refs/heads/main")
base := git(t, work, "rev-parse", "HEAD")
os.WriteFile(filepath.Join(work, "modules", "demo", "hello.txt"), []byte("hi\n"), 0o644)
git(t, work, "add", ".")
git(t, work, "commit", "-q", "-m", "two")
head := git(t, work, "rev-parse", "HEAD")
git(t, work, "push", "-q", bare, "HEAD:refs/pull/1/head")
m.RemoteBase = "file://" + filepath.Join(root, "forge") + "/"
if _, err := tool(t, m, FetchTool)(map[string]any{"repository": "novox/demo", "commit": head, "ref": "refs/pull/1/head", "base": base}); err != nil {
t.Fatal(err)
}
st := waitLong(t, states)
if st.State != delivery.StepPassed {
t.Fatalf("step = %+v", st)
}
var res FetchResult
if err := json.Unmarshal(st.Result, &res); err != nil {
t.Fatal(err)
}
if res.Commit != head || res.Base != base || res.Merge == "" || len(res.Conflicts) != 0 {
t.Errorf("result = %+v, want the head merged cleanly onto the base", res)
}
if _, err := os.Stat(filepath.Join(m.Work, res.Workspace, "tree", "modules", "demo", "Dockerfile")); err != nil {
t.Errorf("the Dockerfile is not in the workspace: %v", err)
}
}
func waitLong(t *testing.T, s *keptStates) delivery.Step {
t.Helper()
select {
case key := <-s.ended:
s.mu.Lock()
defer s.mu.Unlock()
v := s.steps[key]
return v[len(v)-1]
case <-time.After(5 * time.Minute):
t.Fatal("the step never ended on its key")
return delivery.Step{}
}
}
// imageModule writes a workspace "ws1" holding the module "modules/img" with one image artifact and this Dockerfile.
func imageModule(t *testing.T, m *Module, dockerfile string, files map[string]string) {
t.Helper()
dir := filepath.Join(m.Work, "ws1", "tree", "modules", "img")
os.MkdirAll(dir, 0o755)
os.WriteFile(filepath.Join(dir, "module.json"), []byte(`{"module":"img","version":"1","build":{"artifacts":[{"name":"server","kind":"image","from":"Dockerfile"}]}}`), 0o644)
os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(dockerfile), 0o644)
for n, c := range files {
os.MkdirAll(filepath.Dir(filepath.Join(dir, n)), 0o755)
os.WriteFile(filepath.Join(dir, n), []byte(c), 0o644)
}
}
func buildReal(t *testing.T, m *Module, states *keptStates) (BuildResult, delivery.Step) {
t.Helper()
if _, err := tool(t, m, BuildTool)(map[string]any{"workspace": "ws1", "module": "modules/img", "artifact": "server"}); err != nil {
t.Fatal(err)
}
st := waitLong(t, states)
var res BuildResult
if st.State == delivery.StepPassed {
if err := json.Unmarshal(st.Result, &res); err != nil {
t.Fatal(err)
}
}
return res, st
}
// A Dockerfile that adds a file to scratch builds without any network, and the step answers the manifest digest.
func TestARealBuildAnswersTheImagesDigest(t *testing.T) {
needTools(t, "a rootless build", "buildah", "newuidmap")
m, states := realModule(t)
imageModule(t, m, "FROM scratch\nCOPY hello.txt /hello.txt\n", map[string]string{"hello.txt": "hello\n"})
res, st := buildReal(t, m, states)
if st.State != delivery.StepPassed || !digestOK.MatchString(res.Digest) {
t.Fatalf("step = %+v result = %+v", st, res)
}
}
// needBase skips unless the build node can pull the small base the isolation Dockerfiles run in.
func needBase(t *testing.T, m *Module) string {
t.Helper()
const base = "docker.io/library/busybox:1.36"
if err := os.MkdirAll(filepath.Join(m.Store, "xdg"), 0o700); err != nil {
t.Fatal(err)
}
r := m.Run(context.Background(), m.env(), "buildah", append(m.storeFlags(), "pull", base)...)
if r.Status != 0 {
t.Skipf("NOT RUN: the base %s cannot be pulled where this check runs (%s); proven live on a build node", base, firstLine(r.Stderr+r.Err))
}
return base
}
// ADR 0487 §2: the build's process has a non-zero user outside its namespace, no container runtime socket is among its
// mounts, and a step that tries to write outside its workspace fails.
func TestARealBuildIsRootlessWithNoSocketAndNoWayOut(t *testing.T) {
needTools(t, "the rootless isolation", "buildah", "newuidmap")
m, states := realModule(t)
base := needBase(t, m)
// The host side: the namespace the module makes maps the namespace's root to an id other than 0.
r := m.Run(context.Background(), m.env(), "buildah", append(m.storeFlags(), "unshare", "cat", "/proc/self/uid_map")...)
if f := strings.Fields(r.Stdout); r.Status != 0 || len(f) < 3 || f[0] != "0" || f[1] == "0" {
t.Errorf("uid_map in the module's namespace = %q (%s), want 0 mapped to a non-zero id", r.Stdout, r.Stderr)
}
// The inside: a RUN step sees root mapped to a non-zero outer id, and no runtime socket anywhere in its mounts.
imageModule(t, m, "FROM "+base+"\n"+
"RUN awk 'NR==1 { exit ($2 == 0) }' /proc/self/uid_map\n"+
"RUN ! ls /var/run/docker.sock /run/docker.sock /run/podman/podman.sock /run/containerd/containerd.sock 2>/dev/null\n"+
"RUN ! grep -E 'docker.sock|podman.sock|containerd.sock' /proc/self/mountinfo\n", nil)
if _, st := buildReal(t, m, states); st.State != delivery.StepPassed {
t.Errorf("the isolation checks inside the build failed: %+v", st)
}
// A step that writes outside its workspace fails: the context cannot be left, and the host's kernel files are not writable.
imageModule(t, m, "FROM "+base+"\nCOPY ../outside /outside\n", nil)
if _, st := buildReal(t, m, states); st.State != delivery.StepFailed {
t.Errorf("a COPY out of the context passed: %+v", st)
}
imageModule(t, m, "FROM "+base+"\nRUN echo b > /proc/sysrq-trigger\n", nil)
if _, st := buildReal(t, m, states); st.State != delivery.StepFailed {
t.Errorf("a write to the host's /proc/sysrq-trigger passed: %+v", st)
}
}
// ADR 0487 §5, §6: a step that looks for the account's credentials finds nothing: the key in the home's .ssh is in no
// mount of the build.
func TestARealBuildFindsNoCredentialsOfTheAccount(t *testing.T) {
needTools(t, "the credential paths", "buildah", "newuidmap")
m, states := realModule(t)
base := needBase(t, m)
os.MkdirAll(filepath.Join(m.Home, ".ssh"), 0o700)
os.WriteFile(filepath.Join(m.Home, ".ssh", "mesh_ed25519"), []byte("PRIVATE\n"), 0o600)
imageModule(t, m, "FROM "+base+"\n"+
"RUN ! find / -xdev -name mesh_ed25519 2>/dev/null | grep .\n"+
"RUN ! test -e /root/.ssh\n"+
"RUN ! env | grep -E 'REGISTRY_AUTH|MESH_|SSH_AUTH'\n", nil)
if _, st := buildReal(t, m, states); st.State != delivery.StepPassed {
t.Errorf("a step found a credential path of the account: %+v", st)
}
}