Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/container/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ func (r *AppleRuntime) buildCreateArgs(cfg Config) ([]string, error) {
args = append(args, "--volume", mountStr)
}

// Tmpfs mounts (overlays for excluded directories)
// --mount (not --tmpfs) so we can set the mode; --tmpfs accepts no options.
for _, tm := range cfg.TmpfsMounts {
args = append(args, "--tmpfs", tm.Target)
args = append(args, "--mount", fmt.Sprintf("type=tmpfs,destination=%s,mode=%o", tm.Target, tmpfsMode))
}

// Image
Expand Down
4 changes: 2 additions & 2 deletions internal/container/apple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestBuildCreateArgs(t *testing.T) {
{Target: "/workspace/node_modules"},
},
},
want: []string{"create", "--memory", "4096MB", "--dns", "8.8.8.8", "--dns", "8.8.4.4", "--tmpfs", "/workspace/node_modules", "ubuntu:22.04"},
want: []string{"create", "--memory", "4096MB", "--dns", "8.8.8.8", "--dns", "8.8.4.4", "--mount", "type=tmpfs,destination=/workspace/node_modules,mode=1777", "ubuntu:22.04"},
},
{
name: "with volume and tmpfs mounts",
Expand All @@ -188,7 +188,7 @@ func TestBuildCreateArgs(t *testing.T) {
{Target: "/workspace/.venv"},
},
},
want: []string{"create", "--memory", "4096MB", "--dns", "8.8.8.8", "--dns", "8.8.4.4", "--volume", "/home/user/project:/workspace", "--tmpfs", "/workspace/node_modules", "--tmpfs", "/workspace/.venv", "ubuntu:22.04"},
want: []string{"create", "--memory", "4096MB", "--dns", "8.8.8.8", "--dns", "8.8.4.4", "--volume", "/home/user/project:/workspace", "--mount", "type=tmpfs,destination=/workspace/node_modules,mode=1777", "--mount", "type=tmpfs,destination=/workspace/.venv,mode=1777", "ubuntu:22.04"},
},
{
// Apple's container CLI has no --add-host equivalent. ExtraHosts must
Expand Down
58 changes: 30 additions & 28 deletions internal/container/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,36 +161,47 @@ func (r *DockerRuntime) Ping(ctx context.Context) error {
return nil
}

// CreateContainer creates a new Docker container.
func (r *DockerRuntime) CreateContainer(ctx context.Context, cfg Config) (string, error) {
// Verify gVisor is still available if we're configured to use it
if r.ociRuntime == "runsc" && !r.gvisorAvailable() {
return "", fmt.Errorf("gVisor was available at startup but is no longer configured - did Docker daemon configuration change? %w", ErrGVisorNotAvailable)
}

// Pull image if not present
if err := r.ensureImage(ctx, cfg.Image); err != nil {
return "", err
}

// Convert mounts
mounts := make([]mount.Mount, 0, len(cfg.Mounts)+len(cfg.TmpfsMounts))
for _, m := range cfg.Mounts {
// buildContainerMounts converts moat's MountConfig and TmpfsMount entries into
// Docker SDK mount.Mount structs. Tmpfs mounts follow bind mounts so overlays
// of paths inside a bind take effect.
func buildContainerMounts(binds []MountConfig, tmpfs []TmpfsMount) []mount.Mount {
mounts := make([]mount.Mount, 0, len(binds)+len(tmpfs))
for _, m := range binds {
mounts = append(mounts, mount.Mount{
Type: mount.TypeBind,
Source: m.Source,
Target: m.Target,
ReadOnly: m.ReadOnly,
})
}
// Tmpfs mounts — overlays for excluded directories.
// Appended after bind mounts so tmpfs overlays subdirectories of bind-mounted paths.
for _, tm := range cfg.TmpfsMounts {
for _, tm := range tmpfs {
// "exec" overrides runc's default noexec — excluded paths like
// node_modules contain native binaries (turbo, esbuild) that must spawn.
mounts = append(mounts, mount.Mount{
Type: mount.TypeTmpfs,
Target: tm.Target,
TmpfsOptions: &mount.TmpfsOptions{
Mode: tmpfsMode,
Options: [][]string{{"exec"}},
},
})
}
return mounts
}

// CreateContainer creates a new Docker container.
func (r *DockerRuntime) CreateContainer(ctx context.Context, cfg Config) (string, error) {
// Verify gVisor is still available if we're configured to use it
if r.ociRuntime == "runsc" && !r.gvisorAvailable() {
return "", fmt.Errorf("gVisor was available at startup but is no longer configured - did Docker daemon configuration change? %w", ErrGVisorNotAvailable)
}

// Pull image if not present
if err := r.ensureImage(ctx, cfg.Image); err != nil {
return "", err
}

mounts := buildContainerMounts(cfg.Mounts, cfg.TmpfsMounts)

// Default to bridge network if not specified
networkMode := container.NetworkMode(cfg.NetworkMode)
Expand Down Expand Up @@ -999,16 +1010,7 @@ func (m *dockerSidecarManager) StartSidecar(ctx context.Context, cfg SidecarConf
return "", fmt.Errorf("pulling sidecar image: %w", err)
}

// Prepare mounts
mounts := make([]mount.Mount, 0, len(cfg.Mounts))
for _, mt := range cfg.Mounts {
mounts = append(mounts, mount.Mount{
Type: mount.TypeBind,
Source: mt.Source,
Target: mt.Target,
ReadOnly: mt.ReadOnly,
})
}
mounts := buildContainerMounts(cfg.Mounts, nil)

// Create container with labels for orphan cleanup
labels := make(map[string]string)
Expand Down
59 changes: 59 additions & 0 deletions internal/container/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,75 @@ package container
import (
"context"
"os"
"reflect"
"strconv"
"strings"
"sync"
"testing"
"time"

"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)

func TestBuildContainerMounts_TmpfsWritableAndExec(t *testing.T) {
got := buildContainerMounts(nil, []TmpfsMount{{Target: "/workspace/node_modules"}})

if len(got) != 1 {
t.Fatalf("got %d mounts, want 1", len(got))
}
if got[0].Type != mount.TypeTmpfs {
t.Errorf("Type = %v, want %v", got[0].Type, mount.TypeTmpfs)
}
if got[0].TmpfsOptions == nil {
t.Fatal("TmpfsOptions is nil")
}
if got[0].TmpfsOptions.Mode != tmpfsMode {
t.Errorf("Mode = %o, want %o", got[0].TmpfsOptions.Mode, tmpfsMode)
}
wantOpts := [][]string{{"exec"}}
if !reflect.DeepEqual(got[0].TmpfsOptions.Options, wantOpts) {
t.Errorf("Options = %v, want %v", got[0].TmpfsOptions.Options, wantOpts)
}
}

func TestBuildContainerMounts_BindMounts(t *testing.T) {
binds := []MountConfig{
{Source: "/host/project", Target: "/workspace", ReadOnly: false},
{Source: "/host/secret", Target: "/etc/secret", ReadOnly: true},
}

got := buildContainerMounts(binds, nil)

want := []mount.Mount{
{Type: mount.TypeBind, Source: "/host/project", Target: "/workspace", ReadOnly: false},
{Type: mount.TypeBind, Source: "/host/secret", Target: "/etc/secret", ReadOnly: true},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("buildContainerMounts() = %#v, want %#v", got, want)
}
}

// Tmpfs must follow binds in the output slice so overlays of paths inside a
// bind take effect on the daemon side.
func TestBuildContainerMounts_TmpfsAfterBind(t *testing.T) {
binds := []MountConfig{{Source: "/host/project", Target: "/workspace"}}
tmpfs := []TmpfsMount{{Target: "/workspace/node_modules"}}

got := buildContainerMounts(binds, tmpfs)

if len(got) != 2 {
t.Fatalf("got %d mounts, want 2", len(got))
}
if got[0].Type != mount.TypeBind {
t.Errorf("mounts[0].Type = %v, want bind", got[0].Type)
}
if got[1].Type != mount.TypeTmpfs {
t.Errorf("mounts[1].Type = %v, want tmpfs", got[1].Type)
}
}

func TestConfig_GroupAdd(t *testing.T) {
// Verify that GroupAdd field can be set on Config struct
// and is properly typed as []string
Expand Down
5 changes: 5 additions & 0 deletions internal/container/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,11 @@ type TmpfsMount struct {
Target string // absolute container path
}

// tmpfsMode is the permission mode for tmpfs overlays. Sticky world-writable
// (01777) so non-root container users can write — runc otherwise defaults
// tmpfs to 0755 owned by root.
const tmpfsMode = 01777

// ImageInfo contains information about a container image.
type ImageInfo struct {
ID string `json:"id"`
Expand Down
Loading