From e604519afae35a3f7cd6f5c16a161e1cff4a25db Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 18 Aug 2026 21:49:44 +0000 Subject: [PATCH] fix(microsandbox): use msb run --detach with split entrypoint/cmd msb create became boot-only upstream (superradcompany/microsandbox#1298, unreleased), so the microsandbox driver's entrypoint never executed and the Devsy agent never started, even though msb inspect reported the sandbox as running. Switch Create to `msb run --detach`. Instead of joining Entrypoint+Cmd into one string and re-detecting shell-script shapes (fragile: any argv shape microsandbox's CLI --entrypoint can't represent would be silently dropped, running the image default instead), carry Entrypoint (string) and Cmd ([]string) separately on sandboxSpec, the same split driver.RunOptions and the docker driver already use. --entrypoint sets the executable; a trailing "--" plus argv overrides the image CMD while preserving that entrypoint, matching microsandbox's own OCI ENTRYPOINT/CMD resolution exactly. Fixes #1035 --- pkg/driver/microsandbox/cliclient.go | 27 ++++++--- pkg/driver/microsandbox/cliclient_test.go | 60 ++++++++++++++------ pkg/driver/microsandbox/client.go | 3 +- pkg/driver/microsandbox/microsandbox.go | 11 +--- pkg/driver/microsandbox/microsandbox_test.go | 16 +++--- 5 files changed, 75 insertions(+), 42 deletions(-) diff --git a/pkg/driver/microsandbox/cliclient.go b/pkg/driver/microsandbox/cliclient.go index cb473c264..68fa00083 100644 --- a/pkg/driver/microsandbox/cliclient.go +++ b/pkg/driver/microsandbox/cliclient.go @@ -57,7 +57,7 @@ func (c cliClient) Create(ctx context.Context, sandbox string, spec sandboxSpec) if err := c.ensureVolumes(ctx, spec.Mounts); err != nil { return err } - return msbRun(ctx, createArgs(sandbox, spec)...) + return msbRun(ctx, runArgs(sandbox, spec)...) } func (cliClient) Find(ctx context.Context, sandbox string) (*sandboxInfo, error) { @@ -150,19 +150,32 @@ func (cliClient) ensureVolumes(ctx context.Context, mounts []volumeMount) error return nil } -func createArgs(sandbox string, spec sandboxSpec) []string { - args := []string{names.Create, names.Flag(names.Name), sandbox} +const ( + msbCmdRun = "run" + msbFlagDetach = "--detach" +) + +// runArgs builds a detached `msb run` invocation, matching microsandbox's own +// ENTRYPOINT/CMD split: --entrypoint sets the executable, and a trailing "--" +// plus argv overrides the image CMD. +func runArgs(sandbox string, spec sandboxSpec) []string { + args := []string{msbCmdRun, msbFlagDetach, names.Flag(names.Name), sandbox} + if spec.Entrypoint != "" { + args = append(args, "--entrypoint", spec.Entrypoint) + } args = append(args, runtimeArgs(spec)...) args = append(args, resourceArgs(spec)...) args = append(args, mountArgs(spec.Mounts)...) - return append(args, spec.Image) + args = append(args, spec.Image) + if len(spec.Cmd) > 0 { + args = append(args, "--") + args = append(args, spec.Cmd...) + } + return args } func runtimeArgs(spec sandboxSpec) []string { var args []string - if len(spec.Entrypoint) > 0 { - args = append(args, "--entrypoint", strings.Join(spec.Entrypoint, " ")) - } for k, v := range spec.Env { args = append(args, names.Flag(names.Env), k+"="+v) } diff --git a/pkg/driver/microsandbox/cliclient_test.go b/pkg/driver/microsandbox/cliclient_test.go index 569ab1585..8cf5cc624 100644 --- a/pkg/driver/microsandbox/cliclient_test.go +++ b/pkg/driver/microsandbox/cliclient_test.go @@ -9,10 +9,15 @@ import ( "github.com/devsy-org/devsy/pkg/flags/names" ) -func TestCreateArgsFull(t *testing.T) { +func runPrefix() []string { + return []string{msbCmdRun, msbFlagDetach, names.Flag(names.Name), wsName} +} + +func TestRunArgsFull(t *testing.T) { spec := sandboxSpec{ Image: testImg, - Entrypoint: []string{shPath, "-c", "sleep infinity"}, + Entrypoint: shPath, + Cmd: []string{"-c", "sleep infinity", "-"}, Env: map[string]string{"K": "v"}, Labels: map[string]string{"devsy.sh/user": "vscode"}, IdleTimeout: 90 * time.Second, @@ -26,18 +31,23 @@ func TestCreateArgsFull(t *testing.T) { {Target: "/tmp", Tmpfs: true}, }, } - args := createArgs(wsName, spec) + args := runArgs(wsName, spec) + + if !slices.Equal(args[:4], runPrefix()) { + t.Errorf("prefix = %v, want %v", args[:4], runPrefix()) + } - // name comes first, image last. - if args[0] != names.Create || args[1] != names.Flag(names.Name) || args[2] != wsName { - t.Errorf("prefix = %v", args[:3]) + imgIdx := slices.Index(args, testImg) + if imgIdx < 0 { + t.Fatalf("image not found in %v", args) } - if args[len(args)-1] != testImg { - t.Errorf("image should be the final arg, got %q", args[len(args)-1]) + wantSuffix := append([]string{testImg, "--"}, spec.Cmd...) + if !slices.Equal(args[imgIdx:], wantSuffix) { + t.Errorf("image+cmd suffix = %v, want %v", args[imgIdx:], wantSuffix) } - want := [][2]string{ - {"--entrypoint", "/bin/sh -c sleep infinity"}, + wantFlags := [][2]string{ + {"--entrypoint", shPath}, {names.Flag(names.Env), "K=v"}, {"--label", "devsy.sh/user=vscode"}, {"--idle-timeout", "1m30s"}, @@ -48,20 +58,38 @@ func TestCreateArgsFull(t *testing.T) { {"--mount-named", "cache-vol:/cache"}, {"--tmpfs", "/tmp"}, } - for _, kv := range want { + for _, kv := range wantFlags { if !hasFlagValue(args, kv[0], kv[1]) { t.Errorf("missing %s %q in %v", kv[0], kv[1], args) } } - if hasFlag(args, "--net-default-egress") == false { + if !hasFlag(args, "--net-default-egress") { t.Errorf("expected egress deny flag in %v", args) } } -func TestCreateArgsMinimal(t *testing.T) { - args := createArgs(wsName, sandboxSpec{Image: testImg}) - // Only name + image; no sizing/runtime flags for a bare spec. - want := []string{names.Create, names.Flag(names.Name), wsName, testImg} +func TestRunArgsMinimal(t *testing.T) { + args := runArgs(wsName, sandboxSpec{Image: testImg}) + want := append(runPrefix(), testImg) + if !slices.Equal(args, want) { + t.Errorf("args = %v, want %v", args, want) + } +} + +func TestRunArgsCmdWithoutEntrypoint(t *testing.T) { + args := runArgs(wsName, sandboxSpec{Image: testImg, Cmd: []string{"python3", "worker.py"}}) + if hasFlag(args, "--entrypoint") { + t.Errorf("did not expect --entrypoint in %v", args) + } + want := append(runPrefix(), testImg, "--", "python3", "worker.py") + if !slices.Equal(args, want) { + t.Errorf("args = %v, want %v", args, want) + } +} + +func TestRunArgsEntrypointWithoutCmd(t *testing.T) { + args := runArgs(wsName, sandboxSpec{Image: testImg, Entrypoint: shPath}) + want := append(runPrefix(), "--entrypoint", shPath, testImg) if !slices.Equal(args, want) { t.Errorf("args = %v, want %v", args, want) } diff --git a/pkg/driver/microsandbox/client.go b/pkg/driver/microsandbox/client.go index 1528f95a8..8e23ecbe2 100644 --- a/pkg/driver/microsandbox/client.go +++ b/pkg/driver/microsandbox/client.go @@ -8,7 +8,8 @@ import ( type sandboxSpec struct { Image string - Entrypoint []string + Entrypoint string + Cmd []string Memory uint32 CPUs uint8 Env map[string]string diff --git a/pkg/driver/microsandbox/microsandbox.go b/pkg/driver/microsandbox/microsandbox.go index 5f39ac66c..951b8a9e2 100644 --- a/pkg/driver/microsandbox/microsandbox.go +++ b/pkg/driver/microsandbox/microsandbox.go @@ -356,7 +356,8 @@ func (d *microsandboxDriver) buildSpec(workspaceID string, options *driver.RunOp } return sandboxSpec{ Image: options.Image, - Entrypoint: entrypointArgv(options), + Entrypoint: options.Entrypoint, + Cmd: options.Cmd, Memory: d.defaults.memory, CPUs: d.defaults.cpus, Env: options.Env, @@ -437,14 +438,6 @@ func hasUserNSMapping(options *driver.RunOptions) bool { return options.Userns != "" || len(options.UidMap) > 0 || len(options.GidMap) > 0 } -func entrypointArgv(options *driver.RunOptions) []string { - var argv []string - if options.Entrypoint != "" { - argv = append(argv, options.Entrypoint) - } - return append(argv, options.Cmd...) -} - func toContainerDetails(info *sandboxInfo) *config.ContainerDetails { status := "exited" if info.Running { diff --git a/pkg/driver/microsandbox/microsandbox_test.go b/pkg/driver/microsandbox/microsandbox_test.go index 577fc9836..087ff81a5 100644 --- a/pkg/driver/microsandbox/microsandbox_test.go +++ b/pkg/driver/microsandbox/microsandbox_test.go @@ -365,15 +365,13 @@ func TestRunDevContainerSetsEntrypoint(t *testing.T) { if err != nil { t.Fatalf("RunDevContainer: %v", err) } - got := f.created[wsName].Entrypoint - want := []string{shPath, "-c", "start", "-"} - if len(got) != len(want) { - t.Fatalf("entrypoint = %v, want %v", got, want) - } - for i := range want { - if got[i] != want[i] { - t.Fatalf("entrypoint = %v, want %v", got, want) - } + got := f.created[wsName] + if got.Entrypoint != shPath { + t.Errorf("entrypoint = %q, want %q", got.Entrypoint, shPath) + } + want := []string{"-c", "start", "-"} + if !slices.Equal(got.Cmd, want) { + t.Errorf("cmd = %v, want %v", got.Cmd, want) } }