werkator build succeeded after 00:04
Found on mih09 with Werkator 1.37.0 (repository werkrevisor, PR#5): a CI build and a pull-request review started together and both imported the same new rootfs. ## Imports of one image wait for each other `Store.Import` stages in `<image>.tmp` and removes that directory first, so the next import cleans up after an interrupted one. A second import at the same time removed the first one's files: one failed with `latest.tmp/image.json: no such file or directory`, the other's sandbox could not execute `/bin/sh`. An import now holds an exclusive `flock` on `$WERKDOCK_HOME/locks/import-<hash>.lock`, across goroutines and processes; the kernel drops it when the holder dies. The import that waited refuses with `already exists`, as an import after it would. `TestConcurrentImportsOfOneImageLeaveOneCompleteImage`: four imports of a 3000-file archive at once leave one success, three refusals and one complete image. It failed 3 of 3 runs without the lock. ## `werkdock run` answers 127 or 126 `engine.CheckCommand` looks the command up like execvp inside the rootfs (symlinks resolved in the rootfs, PATH in order, a non-executable candidate skipped for a later one). `run` refuses a command that cannot start with docker's 127 (not found) or 126 (not executable) before anything is created; until now bwrap's 1 came back. Paths under a mount, the implicit tmpfs areas included, are not checked. `create`/`start` and the Docker API are unchanged. The resolver moved from `store` to its own package `rootpath`, shared with `engine`. Smoke-tested with the built binary: `run smoke missing` exits 127, a file without execute bit 126, a second import 125 with `already exists`. Version 0.11.0 with its CHANGELOG section. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #13
72 lines
2.8 KiB
Go
72 lines
2.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckCommandAnswersLikeExecvp(t *testing.T) {
|
|
rootfs := t.TempDir()
|
|
for dir, mode := range map[string]os.FileMode{"usr/bin": 0o755, "usr/local/bin": 0o755, "opt/bin": 0o755} {
|
|
if err := os.MkdirAll(filepath.Join(rootfs, dir), mode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for file, mode := range map[string]os.FileMode{
|
|
"usr/bin/busybox": 0o755,
|
|
"usr/bin/dash": 0o755,
|
|
"usr/bin/notes": 0o644,
|
|
"usr/local/bin/tool": 0o644, // shadowed by an executable later in PATH
|
|
"usr/bin/tool": 0o755,
|
|
"usr/local/bin/broken": 0o644,
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(rootfs, file), []byte("#!/bin/sh\n"), mode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// merged /usr with a relative link, and a busybox applet with an absolute one
|
|
for link, target := range map[string]string{"bin": "usr/bin", "usr/bin/sh": "dash", "usr/bin/ash": "/bin/busybox"} {
|
|
if err := os.Symlink(target, filepath.Join(rootfs, link)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
work := []Mount{{Mode: MountBind, Source: t.TempDir(), Dest: "/work"}}
|
|
tests := []struct {
|
|
name string
|
|
command string
|
|
workdir string
|
|
env []EnvVar
|
|
mounts []Mount
|
|
wantCode int // 0: runnable or not known before the start
|
|
}{
|
|
{"a name found in PATH", "sh", "", nil, nil, 0},
|
|
{"an absolute path through the merged-usr link", "/bin/sh", "", nil, nil, 0},
|
|
{"an absolute symlink resolved in the rootfs, not on the host", "ash", "", nil, nil, 0},
|
|
{"a name nowhere in PATH", "missing", "", nil, nil, ExitNotFound},
|
|
{"a path that does not exist", "/bin/missing", "", nil, nil, ExitNotFound},
|
|
{"a file without an execute bit", "notes", "", nil, nil, ExitNotExecutable},
|
|
{"a directory", "/usr/bin", "", nil, nil, ExitNotExecutable},
|
|
{"an executable later in PATH wins over a non-executable earlier", "tool", "", nil, nil, 0},
|
|
{"a non-executable and nothing after it", "broken", "", nil, nil, ExitNotExecutable},
|
|
{"a path under a mount is not checked", "/work/build.sh", "", nil, work, 0},
|
|
{"a relative path from a mounted workdir is not checked", "./build.sh", "/work", nil, work, 0},
|
|
{"a path in an implicit tmpfs is not checked", "/tmp/x", "", nil, nil, 0},
|
|
{"the image's PATH replaces the default", "sh", "", []EnvVar{{Key: "PATH", Value: "/opt/bin"}}, nil, ExitNotFound},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
spec := RunSpec{RootFS: rootfs, Command: []string{tt.command}, Workdir: tt.workdir, Env: tt.env, Mounts: tt.mounts}
|
|
err := CheckCommand(spec)
|
|
var commandErr *CommandError
|
|
switch {
|
|
case tt.wantCode == 0 && err != nil:
|
|
t.Errorf("got %v, want the command to pass", err)
|
|
case tt.wantCode != 0 && (!errors.As(err, &commandErr) || commandErr.Code != tt.wantCode):
|
|
t.Errorf("got %v, want exit code %d", err, tt.wantCode)
|
|
}
|
|
})
|
|
}
|
|
}
|