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…
220 lines
9.1 KiB
Go
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)
|
|
}
|
|
}
|