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
43 lines
1.1 KiB
Go
43 lines
1.1 KiB
Go
package rootpath
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolve(t *testing.T) {
|
|
rootfs := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(rootfs, "usr", "bin"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for link, target := range map[string]string{"bin": "usr/bin", "abs": "/usr", "loop": "loop", "out": "../.."} {
|
|
if err := os.Symlink(target, filepath.Join(rootfs, link)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
tests := []struct{ in, want, wantErr string }{
|
|
{"/bin/sh", "usr/bin/sh", ""},
|
|
{"/abs/bin/x", "usr/bin/x", ""},
|
|
{"/new/dir/file", "new/dir/file", ""},
|
|
{"/../x", "x", ""},
|
|
{"/usr/../etc", "etc", ""},
|
|
{"/", "", ""},
|
|
{"/loop/x", "", "too many levels"},
|
|
{"/out/x", "x", ""}, // .. above the root stays at the root
|
|
}
|
|
for _, tt := range tests {
|
|
got, err := Resolve(rootfs, tt.in)
|
|
if tt.wantErr != "" {
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("%s: got %q, %v; want error %q", tt.in, got, err, tt.wantErr)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil || got != filepath.Join(rootfs, tt.want) {
|
|
t.Errorf("%s: got %q, %v; want %q", tt.in, got, err, filepath.Join(rootfs, tt.want))
|
|
}
|
|
}
|
|
}
|