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:
co-authored by
Claude Fable 5
parent
de79210a7b
commit
e4bfeacf5a
@@ -0,0 +1,124 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"werkdock/internal/engine"
|
||||
)
|
||||
|
||||
func noEnv(string) string { return "" }
|
||||
|
||||
func TestParseRunSupportedFlags(t *testing.T) {
|
||||
opts, err := parseRun([]string{
|
||||
"--rm",
|
||||
"-v", "/repo:/repo",
|
||||
"--volume", "/cache:/root/.gradle:ro",
|
||||
"-e", "CI=true",
|
||||
"-w", "/repo",
|
||||
"buildenv", "sh", "-c", "./gradlew build",
|
||||
}, noEnv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opts.Image != "buildenv" {
|
||||
t.Errorf("image: got %q", opts.Image)
|
||||
}
|
||||
if !reflect.DeepEqual(opts.Command, []string{"sh", "-c", "./gradlew build"}) {
|
||||
t.Errorf("command: got %q", opts.Command)
|
||||
}
|
||||
wantVolumes := []engine.Bind{
|
||||
{Source: "/repo", Dest: "/repo"},
|
||||
{Source: "/cache", Dest: "/root/.gradle", ReadOnly: true},
|
||||
}
|
||||
if !reflect.DeepEqual(opts.Volumes, wantVolumes) {
|
||||
t.Errorf("volumes: got %+v", opts.Volumes)
|
||||
}
|
||||
if !reflect.DeepEqual(opts.Env, []engine.EnvVar{{Key: "CI", Value: "true"}}) {
|
||||
t.Errorf("env: got %+v", opts.Env)
|
||||
}
|
||||
if opts.Workdir != "/repo" {
|
||||
t.Errorf("workdir: got %q", opts.Workdir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunCopiesBareEnvKeysFromTheHost(t *testing.T) {
|
||||
getenv := func(key string) string {
|
||||
if key == "LANG" {
|
||||
return "C.UTF-8"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
opts, err := parseRun([]string{"--rm", "-e", "LANG", "img", "true"}, getenv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(opts.Env, []engine.EnvVar{{Key: "LANG", Value: "C.UTF-8"}}) {
|
||||
t.Errorf("env: got %+v", opts.Env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunRefusesDockerFlagsLoudly(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
wantReason string
|
||||
}{
|
||||
{[]string{"--rm", "-p", "8080:80", "img", "true"}, "no network isolation"},
|
||||
{[]string{"--rm", "--network", "host", "img", "true"}, "network is the host's"},
|
||||
{[]string{"--rm", "--memory", "1g", "img", "true"}, "does not manage resources"},
|
||||
{[]string{"--rm", "--user", "1000", "img", "true"}, "uid 0 mapped to the calling user"},
|
||||
{[]string{"--rm", "-d", "img", "true"}, "not implemented yet"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
_, err := parseRun(tt.args, noEnv)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantReason) {
|
||||
t.Errorf("got %v, want refusal containing %q", err, tt.wantReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunRequiresRmForNow(t *testing.T) {
|
||||
_, err := parseRun([]string{"img", "true"}, noEnv)
|
||||
if err == nil || !strings.Contains(err.Error(), "--rm") {
|
||||
t.Errorf("got %v, want the --rm requirement", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr string
|
||||
}{
|
||||
{"no image", []string{"--rm"}, "no image specified"},
|
||||
{"no command", []string{"--rm", "img"}, "no command specified"},
|
||||
{"volume without dest", []string{"--rm", "-v", "/only-src", "img", "true"}, "expected SRC:DEST"},
|
||||
{"volume with bad option", []string{"--rm", "-v", "/a:/b:rw", "img", "true"}, "only 'ro' is supported"},
|
||||
{"relative volume source", []string{"--rm", "-v", "rel:/b", "img", "true"}, "absolute"},
|
||||
{"relative volume dest", []string{"--rm", "-v", "/a:rel", "img", "true"}, "absolute"},
|
||||
{"relative workdir", []string{"--rm", "-w", "rel", "img", "true"}, "absolute"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := parseRun(tt.args, noEnv)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Errorf("got %v, want it to contain %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunStopsFlagParsingAtTheImage(t *testing.T) {
|
||||
// Docker semantics: everything after the image belongs to the
|
||||
// command, even if it looks like a flag.
|
||||
opts, err := parseRun([]string{"--rm", "img", "ls", "-la", "/tmp"}, noEnv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(opts.Command, []string{"ls", "-la", "/tmp"}) {
|
||||
t.Errorf("command: got %q", opts.Command)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user