//go:build integration && isolation package buildstep import ( "context" "net" "os" "os/exec" "path/filepath" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/forgekey" ) // 1309/S19, the issue's proof: a run step, in bubblewrap as Isolated runs it, reads no key and no login, resolves no // name, reaches no TCP or unix socket of the machine, finds no home of the account, and leaves the shared cache it // wrote unchanged; the workspace it writes keeps what it wrote. It needs bubblewrap and unprivileged user namespaces // (issue 1308) on the machine that runs it, and fails naming them when they are not there. func TestARunStepReachesNothingAndLeavesTheCachesUnchanged(t *testing.T) { if _, err := exec.LookPath(Bubblewrap); err != nil { t.Fatalf("bubblewrap is not installed here, so the isolation of a run step cannot be proved (issue 1309): %v", err) } root := t.TempDir() account := filepath.Join(root, "account") key, login := filepath.Join(account, ".ssh", "id_ed25519"), filepath.Join(account, "bus-login.json") writeFile(t, filepath.Dir(key), filepath.Base(key), "PRIVATE KEY") writeFile(t, account, "bus-login.json", `{"password":"secret"}`) cacheDir := filepath.Join(root, "cache") writeFile(t, cacheDir, "warm", "warm\n") workspace := filepath.Join(root, "work", "tree") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatal(err) } probe := filepath.Join(workspace, "probe") if out, err := exec.Command("go", "build", "-o", probe, "./testdata/isolated-probe").CombinedOutput(); err != nil { t.Fatalf("building the probe: %v\n%s", err, out) } tcp, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer tcp.Close() socket := filepath.Join(account, "bus.sock") unix, err := net.Listen("unix", socket) if err != nil { t.Fatal(err) } defer unix.Close() run := Isolated(forgekey.ExecRunnerFor(0), Isolation{Workspace: workspace, Caches: []string{cacheDir}}) r := run(context.Background(), []string{"PATH=/usr/bin:/bin"}, probe, key, login, tcp.Addr().String(), socket, "localhost.localdomain.invalid", filepath.Join(cacheDir, "warm")) if r.Status != 0 || r.Err != "" || strings.TrimSpace(r.Stdout) != "shut off" { t.Fatalf("the run step was not shut off (status %d, %s):\n%s%s", r.Status, r.Err, r.Stdout, r.Stderr) } if b, _ := os.ReadFile(filepath.Join(cacheDir, "warm")); string(b) != "warm\n" { t.Fatalf("the step changed the shared cache: %q", b) } // The workspace is the step's own: what it writes there stays. r = run(context.Background(), nil, "/bin/sh", "-c", "echo built > out") if b, err := os.ReadFile(filepath.Join(workspace, "out")); r.Status != 0 || err != nil || string(b) != "built\n" { t.Fatalf("the step's write to its workspace did not stay (status %d, %s%s): %q %v", r.Status, r.Stderr, r.Err, b, err) } // 1921/S2: with the ask's cache layer, what one command writes into the cache the next command of the ask reads, // above what the shared cache holds, and the shared cache stays unchanged. layer := filepath.Join(filepath.Dir(workspace), "ws1.cachelayer") asked := Isolated(forgekey.ExecRunnerFor(0), Isolation{Workspace: workspace, Caches: []string{cacheDir}, CacheLayer: layer, LayerCaches: []string{cacheDir}}) r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/sh", "-c", "cat "+filepath.Join(cacheDir, "warm")+" && mkdir -p "+filepath.Join(cacheDir, "ab")+" && echo compiled > "+filepath.Join(cacheDir, "ab", "one-d")) if r.Status != 0 || r.Err != "" || strings.TrimSpace(r.Stdout) != "warm" { t.Fatalf("the first command of the ask did not read the shared cache and write its own (status %d, %s): %s%s", r.Status, r.Err, r.Stdout, r.Stderr) } r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/cat", filepath.Join(cacheDir, "ab", "one-d"), filepath.Join(cacheDir, "warm")) if r.Status != 0 || r.Err != "" || r.Stdout != "compiled\nwarm\n" { t.Fatalf("the next command of the ask did not find what the first wrote (status %d, %s): %q %s", r.Status, r.Err, r.Stdout, r.Stderr) } // A command cannot reach the layer (it lies outside every directory the step binds), and a symlink it writes into // the cache is not kept, so nothing it does makes the keeping write outside the layer. r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/sh", "-c", "ls "+layer+" >/dev/null 2>&1 && echo reached; ln -s "+cacheDir+" "+filepath.Join(cacheDir, "esc")+" && mkdir "+filepath.Join(cacheDir, "cd")+" && echo x > "+filepath.Join(cacheDir, "cd", "f")) if r.Status != 0 || r.Err != "" || strings.Contains(r.Stdout, "reached") { t.Fatalf("the command reached the cache layer or could not write the cache (status %d, %s): %q %s", r.Status, r.Err, r.Stdout, r.Stderr) } if fi, err := os.Lstat(filepath.Join(keptLayer(layer, 0), "esc")); err == nil { t.Fatalf("a symlink the command wrote was kept: %v", fi.Mode()) } if b, err := os.ReadFile(filepath.Join(keptLayer(layer, 0), "cd", "f")); err != nil || string(b) != "x\n" { t.Fatalf("the command's file was not kept: %q %v", b, err) } if entries, _ := os.ReadDir(cacheDir); len(entries) != 1 || entries[0].Name() != "warm" { t.Fatalf("the ask's commands wrote the shared cache: %v", entries) } if err := os.RemoveAll(layer); err != nil { t.Fatalf("the ask's cache layer cannot be removed: %v", err) } }