Werkdock skeleton: doctor, load, run over the bwrap engine (Go)

The minimal build-capable CLI decided in RFC 0002's outcome: stdlib-only
Go module, one static binary.

- engine: RunSpec behind the Engine interface (RFC 0001); the Bwrap
  engine ports Werkator's hardened invocation — uid-0 mapping, read-only
  rootfs at /, proc/dev/tmp/root before the user binds so binds below
  them land inside, mountpoint pre-creation in the rootfs including file
  mountpoints, and a guard against binds escaping the rootfs. --clearenv
  gives docker-style clean environments (HOME/PATH set explicitly).
- store: images under $WERKDOCK_HOME (default ~/.werkdock), load
  unpacks via the tar CLI into a tmp dir and renames atomically.
- cli: docker-shaped run flags (-v/-e/-w/--rm); refused docker flags
  (-p, --network, --memory, --cpus, --user, -d) fail loudly with the
  reason; exit codes follow docker (125 CLI errors, child code through).
- doctor: port of werkator-build-prerequisites.sh — userns probe with
  the three signals, tar/zstd, free space and group-quota headroom via
  testable df/quota parsers, same PASS/FAIL output.
- tests: argv golden test, mountpoint and escape tests, flag refusals,
  store round trip, doctor parsers — plus real-sandbox integration
  tests that skip where bwrap or userns are unavailable.

Also records in step 21: RFC 0002 levels 2/3 deferred; next goal is
sandbox builds of Werkator, Werkbaum, and Werkdock itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-01 07:01:21 +02:00
co-authored by Claude Fable 5
parent de79210a7b
commit e4bfeacf5a
17 changed files with 1628 additions and 2 deletions
+130
View File
@@ -0,0 +1,130 @@
package store
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// writeTestArchive builds a minimal rootfs .tar.gz with the stdlib, so
// the tests need no zstd; Load unpacks it with the system tar.
func writeTestArchive(t *testing.T, path string) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
gz := gzip.NewWriter(f)
tw := tar.NewWriter(gz)
if err := tw.WriteHeader(&tar.Header{Name: "etc/", Mode: 0o755, Typeflag: tar.TypeDir}); err != nil {
t.Fatal(err)
}
content := []byte("hello from the rootfs\n")
if err := tw.WriteHeader(&tar.Header{Name: "etc/hello", Mode: 0o644, Size: int64(len(content))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(content); err != nil {
t.Fatal(err)
}
for _, c := range []interface{ Close() error }{tw, gz, f} {
if err := c.Close(); err != nil {
t.Fatal(err)
}
}
}
func TestLoadUnpacksArchiveIntoTheStore(t *testing.T) {
if _, err := exec.LookPath("tar"); err != nil {
t.Skip("tar not installed")
}
st := Store{Root: t.TempDir()}
archive := filepath.Join(t.TempDir(), "mini-rootfs.tar.gz")
writeTestArchive(t, archive)
if err := st.Load(archive, "mini"); err != nil {
t.Fatal(err)
}
rootfs, err := st.RootFS("mini")
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(filepath.Join(rootfs, "etc", "hello"))
if err != nil || string(content) != "hello from the rootfs\n" {
t.Errorf("unpacked file: %q, %v", content, err)
}
metaRaw, err := os.ReadFile(filepath.Join(st.Root, "images", "mini", "image.json"))
if err != nil {
t.Fatal(err)
}
var meta ImageMeta
if err := json.Unmarshal(metaRaw, &meta); err != nil {
t.Fatal(err)
}
if meta.Name != "mini" || meta.Source == "" || meta.CreatedAt.IsZero() {
t.Errorf("image.json incomplete: %+v", meta)
}
}
func TestLoadRefusesAnExistingImageName(t *testing.T) {
if _, err := exec.LookPath("tar"); err != nil {
t.Skip("tar not installed")
}
st := Store{Root: t.TempDir()}
archive := filepath.Join(t.TempDir(), "mini.tar.gz")
writeTestArchive(t, archive)
if err := st.Load(archive, "mini"); err != nil {
t.Fatal(err)
}
err := st.Load(archive, "mini")
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Errorf("got %v, want an already-exists refusal", err)
}
}
func TestLoadLeavesNoHalfImageOnFailure(t *testing.T) {
if _, err := exec.LookPath("tar"); err != nil {
t.Skip("tar not installed")
}
st := Store{Root: t.TempDir()}
broken := filepath.Join(t.TempDir(), "broken.tar.gz")
if err := os.WriteFile(broken, []byte("this is not a tar archive"), 0o644); err != nil {
t.Fatal(err)
}
if err := st.Load(broken, "broken"); err == nil {
t.Fatal("expected the load to fail")
}
if _, err := os.Stat(filepath.Join(st.Root, "images", "broken")); !os.IsNotExist(err) {
t.Errorf("expected no image directory, got %v", err)
}
if _, err := os.Stat(filepath.Join(st.Root, "images", "broken.tmp")); !os.IsNotExist(err) {
t.Errorf("expected no leftover tmp directory, got %v", err)
}
}
func TestRootFSValidation(t *testing.T) {
st := Store{Root: t.TempDir()}
if _, err := st.RootFS("no-such-image"); err == nil || !strings.Contains(err.Error(), "no such image") {
t.Errorf("got %v, want a no-such-image error", err)
}
if _, err := st.RootFS("../escape"); err == nil || !strings.Contains(err.Error(), "invalid image name") {
t.Errorf("got %v, want an invalid-name error", err)
}
}
func TestImageNameFromArchive(t *testing.T) {
tests := []struct{ in, want string }{
{"werkator-buildenv-trixie.tar.zst", "werkator-buildenv-trixie"},
{"/path/to/Base.TAR.GZ", "base"},
{"rootfs.tgz", "rootfs"},
{"plain", "plain"},
}
for _, tt := range tests {
if got := ImageNameFromArchive(tt.in); got != tt.want {
t.Errorf("ImageNameFromArchive(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}