werkdock: ordered mounts with --tmpfs, images verb, :rw accepted
Session C groundwork: Werkator's git-metadata mask needs a tmpfs BETWEEN binds (ro-bind .git, tmpfs .git/werkator, bind workspace), so -v and --tmpfs now collect into one ordered mount list and RunSpec carries Mounts instead of Binds. --tmpfs DEST is the docker flag of the same name. `werkdock images` lists loaded image names one per line, so a consumer can check existence through the CLI. -v accepts the explicit :rw docker default instead of refusing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
263245d49e
commit
a2da6454f7
+22
-11
@@ -65,15 +65,20 @@ func (b *Bwrap) Argv(spec RunSpec) ([]string, error) {
|
||||
"--tmpfs", "/tmp",
|
||||
"--tmpfs", "/root",
|
||||
}
|
||||
for _, bd := range spec.Binds {
|
||||
if !filepath.IsAbs(bd.Dest) {
|
||||
return nil, fmt.Errorf("bind destination must be an absolute path: %s", bd.Dest)
|
||||
for _, m := range spec.Mounts {
|
||||
if !filepath.IsAbs(m.Dest) {
|
||||
return nil, fmt.Errorf("mount destination must be an absolute path: %s", m.Dest)
|
||||
}
|
||||
flag := "--bind"
|
||||
if bd.ReadOnly {
|
||||
flag = "--ro-bind"
|
||||
switch m.Mode {
|
||||
case MountBind:
|
||||
args = append(args, "--bind", m.Source, m.Dest)
|
||||
case MountRoBind:
|
||||
args = append(args, "--ro-bind", m.Source, m.Dest)
|
||||
case MountTmpfs:
|
||||
args = append(args, "--tmpfs", m.Dest)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown mount mode %d for %s", m.Mode, m.Dest)
|
||||
}
|
||||
args = append(args, flag, bd.Source, bd.Dest)
|
||||
}
|
||||
args = append(args,
|
||||
"--clearenv",
|
||||
@@ -108,17 +113,23 @@ func EnsureMountpoints(spec RunSpec) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, bd := range spec.Binds {
|
||||
target, err := rootfsPath(spec.RootFS, bd.Dest)
|
||||
for _, m := range spec.Mounts {
|
||||
target, err := rootfsPath(spec.RootFS, m.Dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Lstat(target); err == nil {
|
||||
continue
|
||||
}
|
||||
src, err := os.Stat(bd.Source)
|
||||
if m.Mode == MountTmpfs {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
src, err := os.Stat(m.Source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bind source %s: %w", bd.Source, err)
|
||||
return fmt.Errorf("bind source %s: %w", m.Source, err)
|
||||
}
|
||||
if src.Mode().IsRegular() {
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
|
||||
@@ -14,10 +14,12 @@ func TestArgvAssemblesTheHardenedInvocation(t *testing.T) {
|
||||
b := &Bwrap{}
|
||||
spec := RunSpec{
|
||||
RootFS: "/store/images/buildenv/rootfs",
|
||||
Binds: []Bind{
|
||||
{Source: "/etc/resolv.conf", Dest: "/etc/resolv.conf", ReadOnly: true},
|
||||
{Source: "/repo", Dest: "/repo"},
|
||||
{Source: "/cache", Dest: "/root/.gradle"},
|
||||
Mounts: []Mount{
|
||||
{Mode: MountRoBind, Source: "/etc/resolv.conf", Dest: "/etc/resolv.conf"},
|
||||
{Mode: MountRoBind, Source: "/repo/.git", Dest: "/repo/.git"},
|
||||
{Mode: MountTmpfs, Dest: "/repo/.git/werkator"},
|
||||
{Mode: MountBind, Source: "/repo", Dest: "/repo"},
|
||||
{Mode: MountBind, Source: "/cache", Dest: "/root/.gradle"},
|
||||
},
|
||||
Env: []EnvVar{{Key: "CI", Value: "true"}, {Key: "TERM", Value: "dumb"}},
|
||||
Workdir: "/repo",
|
||||
@@ -34,6 +36,8 @@ func TestArgvAssemblesTheHardenedInvocation(t *testing.T) {
|
||||
"--ro-bind", "/store/images/buildenv/rootfs", "/",
|
||||
"--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--tmpfs", "/root",
|
||||
"--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf",
|
||||
"--ro-bind", "/repo/.git", "/repo/.git",
|
||||
"--tmpfs", "/repo/.git/werkator",
|
||||
"--bind", "/repo", "/repo",
|
||||
"--bind", "/cache", "/root/.gradle",
|
||||
"--clearenv",
|
||||
@@ -59,8 +63,8 @@ func TestArgvValidation(t *testing.T) {
|
||||
{"relative rootfs", RunSpec{RootFS: "rootfs", Command: []string{"true"}}, "absolute"},
|
||||
{"missing command", RunSpec{RootFS: "/r"}, "no command specified"},
|
||||
{
|
||||
"relative bind dest",
|
||||
RunSpec{RootFS: "/r", Binds: []Bind{{Source: "/s", Dest: "work"}}, Command: []string{"true"}},
|
||||
"relative mount dest",
|
||||
RunSpec{RootFS: "/r", Mounts: []Mount{{Mode: MountBind, Source: "/s", Dest: "work"}}, Command: []string{"true"}},
|
||||
"absolute",
|
||||
},
|
||||
}
|
||||
@@ -103,17 +107,18 @@ func TestEnsureMountpointsCreatesMissingAndSkipsExisting(t *testing.T) {
|
||||
}
|
||||
spec := RunSpec{
|
||||
RootFS: rootfs,
|
||||
Binds: []Bind{
|
||||
{Source: "/etc", Dest: "/etc/resolv.conf", ReadOnly: true}, // exists: skipped (source type irrelevant)
|
||||
{Source: srcDir, Dest: "/repo/workspace"}, // missing dir mountpoint
|
||||
{Source: srcFile, Dest: "/etc/hosts.werkdock"}, // missing file mountpoint
|
||||
Mounts: []Mount{
|
||||
{Mode: MountRoBind, Source: "/etc", Dest: "/etc/resolv.conf"}, // exists: skipped (source type irrelevant)
|
||||
{Mode: MountBind, Source: srcDir, Dest: "/repo/workspace"}, // missing dir mountpoint
|
||||
{Mode: MountBind, Source: srcFile, Dest: "/etc/hosts.werkdock"}, // missing file mountpoint
|
||||
{Mode: MountTmpfs, Dest: "/repo/.git/werkator"}, // tmpfs mountpoint, no source
|
||||
},
|
||||
Command: []string{"true"},
|
||||
}
|
||||
if err := EnsureMountpoints(spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, dir := range []string{"proc", "dev", "tmp", "root", "repo/workspace"} {
|
||||
for _, dir := range []string{"proc", "dev", "tmp", "root", "repo/workspace", "repo/.git/werkator"} {
|
||||
fi, err := os.Stat(filepath.Join(rootfs, dir))
|
||||
if err != nil || !fi.IsDir() {
|
||||
t.Errorf("expected directory mountpoint %s in the rootfs: %v", dir, err)
|
||||
@@ -132,7 +137,7 @@ func TestEnsureMountpointsCreatesMissingAndSkipsExisting(t *testing.T) {
|
||||
func TestEnsureMountpointsRefusesEscapingDestinations(t *testing.T) {
|
||||
spec := RunSpec{
|
||||
RootFS: t.TempDir(),
|
||||
Binds: []Bind{{Source: "/tmp", Dest: "/../outside"}},
|
||||
Mounts: []Mount{{Mode: MountBind, Source: "/tmp", Dest: "/../outside"}},
|
||||
Command: []string{"true"},
|
||||
}
|
||||
err := EnsureMountpoints(spec)
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
// logic without duplicating it (RFC 0002).
|
||||
package engine
|
||||
|
||||
// Bind is one bind mount, applied in order; later mounts shadow earlier
|
||||
// ones at their own path, exactly as bwrap layers them.
|
||||
type Bind struct {
|
||||
Source string
|
||||
Dest string
|
||||
ReadOnly bool
|
||||
// MountMode distinguishes the mount kinds a RunSpec can carry.
|
||||
type MountMode int
|
||||
|
||||
const (
|
||||
// MountBind is a read-write bind mount.
|
||||
MountBind MountMode = iota
|
||||
// MountRoBind is a read-only bind mount.
|
||||
MountRoBind
|
||||
// MountTmpfs is an empty tmpfs at Dest; Source is unused.
|
||||
MountTmpfs
|
||||
)
|
||||
|
||||
// Mount is one mount, applied in order; later mounts shadow earlier
|
||||
// ones at their own path, exactly as bwrap layers them — the order of
|
||||
// -v and --tmpfs flags is therefore significant and preserved.
|
||||
type Mount struct {
|
||||
Mode MountMode
|
||||
Source string
|
||||
Dest string
|
||||
}
|
||||
|
||||
// EnvVar is one environment variable; order is preserved.
|
||||
@@ -23,7 +36,7 @@ type RunSpec struct {
|
||||
// RootFS is the absolute path to the unpacked image rootfs,
|
||||
// bound read-only at /.
|
||||
RootFS string
|
||||
Binds []Bind
|
||||
Mounts []Mount
|
||||
Env []EnvVar
|
||||
Workdir string
|
||||
Command []string
|
||||
|
||||
Reference in New Issue
Block a user