Files
miandmhoennig ea30cbe79c
werkator build succeeded after 00:04
One import per image at a time, and run answers 126/127 for a command that cannot start (0.11.0) (#13)
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
2026-10-05 17:29:20 +02:00

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))
}
}
}