Skip to content
Merged
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
31 changes: 31 additions & 0 deletions e2e/tests/up/provider_microsandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package up

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"

"github.com/devsy-org/devsy/e2e/framework"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

const osLinux = "linux"
Expand Down Expand Up @@ -69,6 +72,34 @@ var _ = ginkgo.Describe(
err = f.DevsySSHEchoTestString(ctx, tempDir)
framework.ExpectNoError(err)

workspacePath := filepath.Join("/workspaces", filepath.Base(tempDir))
guestFile := filepath.Join(tempDir, "guest-created.txt")
_, err = f.DevsySSHOnce(ctx, tempDir, fmt.Sprintf(
"touch %s/guest-created.txt && chmod 0644 %s/guest-created.txt",
workspacePath, workspacePath,
))
framework.ExpectNoError(err)
guestInfo, err := os.Stat(guestFile)
framework.ExpectNoError(err)
gomega.Expect(guestInfo.Mode().Perm()).To(gomega.Equal(os.FileMode(0o644)))

hostFile := filepath.Join(tempDir, "host-created.txt")
//nolint:gosec // permission mirroring is the behavior under test
err = os.WriteFile(
hostFile,
[]byte("host"),
0o644,
)
framework.ExpectNoError(err)
//nolint:gosec // permission mirroring is the behavior under test
err = os.Chmod(hostFile, 0o644)
framework.ExpectNoError(err)
owner, err := f.DevsySSHOnce(ctx, tempDir, fmt.Sprintf(
"stat -c '%%U %%a' %s/host-created.txt", workspacePath,
))
framework.ExpectNoError(err)
gomega.Expect(owner).To(gomega.ContainSubstring("vscode 644"))

// stop, then bring it back up and confirm it is reachable again
err = f.DevsyWorkspaceStop(ctx, tempDir)
framework.ExpectNoError(err)
Expand Down
4 changes: 3 additions & 1 deletion e2e/tests/up/testdata/microsandbox/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"name": "microsandbox",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"containerUser": "vscode",
"remoteUser": "vscode"
}
21 changes: 16 additions & 5 deletions pkg/driver/microsandbox/cliclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ func (cliClient) EnsureInstalled(_ context.Context) error {
)
}

func (cliClient) Version(ctx context.Context) (string, error) {
// #nosec G204 -- args are a resolved binary path and fixed version command
out, err := exec.CommandContext(ctx, msbBinary(), "--version").CombinedOutput()
if err != nil {
return "", fmt.Errorf(
"get microsandbox version: %s: %w",
strings.TrimSpace(string(out)), err,
)
}
return strings.TrimSpace(string(out)), nil
}

func (cliClient) EnsureImage(ctx context.Context, imageRef string) error {
if dockerImageExists(ctx, imageRef) {
return loadFromDocker(ctx, imageRef)
Expand Down Expand Up @@ -178,6 +190,9 @@ func runArgs(sandbox string, spec sandboxSpec) []string {

func runtimeArgs(spec sandboxSpec) []string {
var args []string
if spec.User != "" {
args = append(args, "--user", spec.User)
}
for k, v := range spec.Env {
args = append(args, names.Flag(names.Env), k+"="+v)
}
Expand Down Expand Up @@ -228,11 +243,7 @@ func mountArgs(mounts []volumeMount) []string {
case m.Volume != "":
args = append(args, "--mount-named", m.Volume+":"+m.Target)
case m.Source != "":
spec := m.Source + ":" + m.Target
if m.ReadOnly {
spec += ":ro"
}
args = append(args, "--mount-dir", spec)
args = append(args, "--mount-dir", bindMountSpec(m))
}
}
return args
Expand Down
33 changes: 33 additions & 0 deletions pkg/driver/microsandbox/cliclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ func TestRunArgsMinimal(t *testing.T) {
}
}

func TestRunArgsPropagatesUser(t *testing.T) {
args := runArgs(wsName, sandboxSpec{Image: testImg, User: "vscode"})
if !hasFlagValue(args, "--user", "vscode") {
t.Fatalf("args = %v, want --user vscode", args)
}
}

func TestRunArgsCmdWithoutEntrypoint(t *testing.T) {
args := runArgs(wsName, sandboxSpec{Image: testImg, Cmd: []string{"python3", "worker.py"}})
if hasFlag(args, "--entrypoint") {
Expand Down Expand Up @@ -117,6 +124,32 @@ func TestMountArgsAndNamedVolumes(t *testing.T) {
}
}

func TestMountArgsWorkspacePolicyAndOwner(t *testing.T) {
args := mountArgs([]volumeMount{
{Target: testBindDst, Source: testBindSrc, Policy: mountPolicy{
StatVirtualization: statVirtStrict,
HostPermissions: hostPermissionsMirror,
Owner: &mountOwner{UID: 1000, GID: 1001},
}},
})
want := testBindSrc + ":" + testBindDst + ":stat-virt=strict,host-perms=mirror,uid=1000,gid=1001"
if !hasFlagValue(args, "--mount-dir", want) {
t.Fatalf("mountArgs = %v, want %q", args, want)
}
}

func TestParseMicrosandboxVersion(t *testing.T) {
for _, input := range []string{"microsandbox " + testVersion, "msb " + testVersion, "v" + testVersion, testVersion} {
version, err := parseMicrosandboxVersion(input)
if err != nil || version.String() != testVersion {
t.Errorf("parseMicrosandboxVersion(%q) = %v, %v", input, version, err)
}
}
if _, err := parseMicrosandboxVersion("not a version"); err == nil {
t.Error("malformed version should fail")
}
}

func TestResourceArgsOmitsZero(t *testing.T) {
if got := resourceArgs(sandboxSpec{}); len(got) != 0 {
t.Errorf("zero spec should produce no resource args, got %v", got)
Expand Down
3 changes: 3 additions & 0 deletions pkg/driver/microsandbox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

type sandboxSpec struct {
Image string
User string
Entrypoint string
Cmd []string
Memory uint32
Expand All @@ -29,6 +30,7 @@ type volumeMount struct {
Volume string
Tmpfs bool
ReadOnly bool
Policy mountPolicy
}

type sandboxInfo struct {
Expand All @@ -52,6 +54,7 @@ type execRequest struct {
// job, not the client's.
type sandboxClient interface {
EnsureInstalled(ctx context.Context) error
Version(ctx context.Context) (string, error)
EnsureImage(ctx context.Context, image string) error
Create(ctx context.Context, name string, spec sandboxSpec) error
Find(ctx context.Context, name string) (*sandboxInfo, error)
Expand Down
104 changes: 60 additions & 44 deletions pkg/driver/microsandbox/microsandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
"fmt"
"io"
"math"
"regexp"
"runtime"
"strconv"
"strings"
"time"

"github.com/blang/semver/v4"
pkgconfig "github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/driver"
Expand All @@ -38,11 +40,12 @@ type specDefaults struct {
}

type microsandboxDriver struct {
client sandboxClient
idLabels []string
defaults specDefaults
workspaceInfo *provider.AgentWorkspaceInfo
docker driver.ImageDriver
client sandboxClient
idLabels []string
defaults specDefaults
workspaceInfo *provider.AgentWorkspaceInfo
docker driver.ImageDriver
workspaceMountPolicy workspaceMountPolicy
}

var (
Expand All @@ -52,12 +55,47 @@ var (
_ driver.Preflighter = (*microsandboxDriver)(nil)
)

var minimumMicrosandboxVersion = semver.MustParse("0.7.2")

var microsandboxVersionPattern = regexp.MustCompile(
`(?:^|[^0-9])v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)`,
)

func parseMicrosandboxVersion(output string) (semver.Version, error) {
matches := microsandboxVersionPattern.FindStringSubmatch(output)
if len(matches) != 2 {
return semver.Version{}, fmt.Errorf("unable to parse microsandbox version from %q", output)
}
version, err := semver.Parse(matches[1])
if err != nil {
return semver.Version{}, fmt.Errorf(
"unable to parse microsandbox version from %q: %w", output, err,
)
}
return version, nil
}

// Preflight checks the microsandbox runtime binary is installed. There is no
// daemon to auto-start, so a missing binary is surfaced for the user.
func (d *microsandboxDriver) Preflight(ctx context.Context, _ driver.PreflightOptions) error {
if err := d.client.EnsureInstalled(ctx); err != nil {
return &driver.PreflightError{Provider: provider.MicrosandboxDriver, Err: err}
}
rawVersion, err := d.client.Version(ctx)
if err != nil {
return &driver.PreflightError{Provider: provider.MicrosandboxDriver, Err: err}
}
version, err := parseMicrosandboxVersion(rawVersion)
if err != nil {
return &driver.PreflightError{Provider: provider.MicrosandboxDriver, Err: err}
}
if version.LT(minimumMicrosandboxVersion) {
Comment on lines +84 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Version gate blocks cleanup

If a user has MicroSandbox v0.6.14 and an existing VM, this shared preflight rejects the runtime before every runner operation. Because stop, delete, status, and logs all construct a runner first, users cannot manage or clean up that VM through Devsy. Apply the version requirement only to operations that need the new mount policies, or allow lifecycle cleanup against older runtimes.

Knowledge Base Used: Supported provider backends

return &driver.PreflightError{Provider: provider.MicrosandboxDriver, Err: fmt.Errorf(
"microsandbox %s is too old for Devsy workspace ownership synchronization; "+
"v%s or newer is required. Update with `msb self update`",
version, minimumMicrosandboxVersion,
)}
}
return nil
}

Expand All @@ -84,18 +122,29 @@ func NewMicrosandboxDriver(
blockEgress: cfg.BlockEgress == pkgconfig.BoolTrue,
rootDiskGB: parseUint32(cfg.Storage),
}
workspacePolicy, err := parseWorkspaceMountPolicy(cfg)
if err != nil {
return nil, err
}

log.Debugf(
"using microsandbox driver: memory=%dMiB cpus=%d ephemeral=%t idleTimeout=%s",
defaults.memory, defaults.cpus, defaults.ephemeral, defaults.idleTimeout,
)
d := newDriver(client, workspaceInfo.CLIOptions.IDLabels, defaults)
d.workspaceMountPolicy = workspacePolicy
d.workspaceInfo = workspaceInfo
return d, nil
}

func newDriver(client sandboxClient, idLabels []string, defaults specDefaults) *microsandboxDriver {
return &microsandboxDriver{client: client, idLabels: idLabels, defaults: defaults}
return &microsandboxDriver{
client: client, idLabels: idLabels, defaults: defaults,
workspaceMountPolicy: workspaceMountPolicy{
StatVirtualization: statVirtStrict,
HostPermissions: hostPermissionsMirror,
},
}
}

func (d *microsandboxDriver) RunDevContainer(
Expand Down Expand Up @@ -254,6 +303,9 @@ func (d *microsandboxDriver) UpdateContainerUserUID(
_ *config.DevContainerConfig,
_ io.Writer,
) error {
// UpdateContainerUserUID is intentionally a no-op for MicroSandbox.
// Docker rewrites container identity to match the host for bind mounts;
// MicroSandbox virtualizes ownership on virtiofs mounts instead.
return nil
}

Expand Down Expand Up @@ -381,6 +433,7 @@ func (d *microsandboxDriver) buildSpec(
}
return sandboxSpec{
Image: options.Image,
User: options.User,
Entrypoint: options.Entrypoint,
Cmd: options.Cmd,
Memory: memory,
Expand All @@ -389,51 +442,14 @@ func (d *microsandboxDriver) buildSpec(
Labels: labels,
Ephemeral: d.defaults.ephemeral,
IdleTimeout: d.defaults.idleTimeout,
Mounts: volumeMounts(options),
Mounts: d.volumeMounts(options),
MaxMemory: d.defaults.maxMemory,
MaxCPUs: d.defaults.maxCPUs,
BlockEgress: d.defaults.blockEgress,
RootDiskGB: rootDiskGB,
}
}

func volumeMounts(options *driver.RunOptions) []volumeMount {
var out []volumeMount
if b := bindMount(options.WorkspaceMount); b != nil {
out = append(out, *b)
}
for _, m := range options.Mounts {
if vm, ok := toVolumeMount(m); ok {
out = append(out, vm)
}
}
return out
}

func toVolumeMount(m *config.Mount) (volumeMount, bool) {
if m == nil || m.Target == "" {
return volumeMount{}, false
}
switch m.Type {
case driver.MountTypeBind:
if b := bindMount(m); b != nil {
return *b, true
}
case driver.MountTypeVolume:
return volumeMount{Target: m.Target, Volume: m.Source}, true
case driver.MountTypeTmpfs:
return volumeMount{Target: m.Target, Tmpfs: true}, true
}
return volumeMount{}, false
}

func bindMount(m *config.Mount) *volumeMount {
if m == nil || m.Source == "" || m.Target == "" {
return nil
}
return &volumeMount{Target: m.Target, Source: m.Source, ReadOnly: m.IsReadOnly()}
}

func warnUnsupportedOptions(options *driver.RunOptions) {
var ignored []string
if isPrivileged(options) {
Expand Down
Loading
Loading