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
15 changes: 15 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,13 @@ jobs:
install-kind: true
requires-secret: false

- label: up-provider-microsandbox
runner: ubuntu-latest
free-disk-space: true
install-kind: false
requires-secret: false
install-microsandbox: true

# Up Docker Compose tests

- label: up-docker-compose
Expand Down Expand Up @@ -599,6 +606,14 @@ jobs:
echo "DOCKER_HOST=unix:///run/podman/podman.sock" >> "$GITHUB_ENV"
sudo podman info

- name: Install microsandbox (Linux)
if: matrix.install-microsandbox == true && runner.os == 'Linux'
run: |
curl -fsSL https://install.microsandbox.dev | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# The e2e test skips gracefully if KVM is unavailable on the runner.
"$HOME/.local/bin/msb" doctor || true
Comment on lines +609 to +615

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Install msb on the root execution path.

The Linux tests run through sudo (Line 644), whose secure PATH excludes $HOME/.local/bin; the microsandbox driver in the root-run e2e process will therefore fail to locate msb. Install or symlink it into /usr/local/bin after installation.

Proposed fix
           curl -fsSL https://install.microsandbox.dev | sh
-          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+          sudo ln -sf "$HOME/.local/bin/msb" /usr/local/bin/msb
           # The e2e test skips gracefully if KVM is unavailable on the runner.
-          "$HOME/.local/bin/msb" doctor || true
+          /usr/local/bin/msb doctor || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Install microsandbox (Linux)
if: matrix.install-microsandbox == true && runner.os == 'Linux'
run: |
curl -fsSL https://install.microsandbox.dev | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# The e2e test skips gracefully if KVM is unavailable on the runner.
"$HOME/.local/bin/msb" doctor || true
- name: Install microsandbox (Linux)
if: matrix.install-microsandbox == true && runner.os == 'Linux'
run: |
curl -fsSL https://install.microsandbox.dev | sh
sudo ln -sf "$HOME/.local/bin/msb" /usr/local/bin/msb
# The e2e test skips gracefully if KVM is unavailable on the runner.
/usr/local/bin/msb doctor || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-ci.yml around lines 609 - 615, Update the “Install
microsandbox (Linux)” workflow step to make msb available on the root sudo
execution path by installing or symlinking the downloaded binary into
/usr/local/bin after the existing installer runs. Keep the current GITHUB_PATH
update and doctor check unchanged.


- name: remove docker
if: matrix.label == 'docker-install' && (matrix.requires-secret == false || needs.can-read-secret.outputs.secret-set == 'true')
run: |
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/renderer/public/icons/providers/microsandbox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const ICON_MAP: Record<string, string> = {
kubernetes: "./icons/providers/kubernetes.svg",
k8s: "./icons/providers/kubernetes.svg",
k3s: "./icons/providers/kubernetes.svg",
microsandbox: "./icons/providers/microsandbox.svg",
microvm: "./icons/providers/microsandbox.svg",
digitalocean: "./icons/providers/digitalocean.svg",
ssh: "./icons/providers/ssh.svg",
civo: "./icons/providers/civo.svg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const PRESETS = [
{ name: "docker", description: "Local Docker containers" },
{ name: "podman", description: "Local Podman containers" },
{ name: "apple", description: "Apple containers (macOS 26+, Apple silicon)" },
{ name: "microsandbox", description: "Hardware-isolated microVMs" },
{ name: "lima", description: "Linux VMs via Lima (macOS)" },
{ name: "orbstack", description: "Fast containers and VMs via OrbStack (macOS)" },
{ name: "ssh", description: "Remote SSH machines" },
Expand Down
83 changes: 83 additions & 0 deletions e2e/tests/up/provider_microsandbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package up

import (
"context"
"os"
"os/exec"
"runtime"

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

const osLinux = "linux"

// skipIfNoMicrosandbox skips when the microsandbox runtime or hardware
// virtualization is unavailable, mirroring how other providers guard on their
// runtime being present.
func skipIfNoMicrosandbox() {
if _, err := exec.LookPath("msb"); err != nil {
ginkgo.Skip("microsandbox runtime (msb) not found on PATH")
}
switch {
case runtime.GOOS == osLinux:
kvm, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0)
if err != nil {
ginkgo.Skip("microsandbox requires KVM (/dev/kvm not accessible)")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
_ = kvm.Close()
case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64":
// Apple silicon supports microsandbox via the hypervisor framework.
default:
ginkgo.Skip("microsandbox requires Apple silicon or Linux with KVM")
}
}

var _ = ginkgo.Describe(
"testing up command for microsandbox provider",
ginkgo.Label("up-provider-microsandbox"),
func() {
var initialDir string

ginkgo.BeforeEach(func() {
skipIfNoMicrosandbox()
var err error
initialDir, err = os.Getwd()
framework.ExpectNoError(err)
})

ginkgo.It("runs devsy in a microsandbox microVM", func(ctx context.Context) {
f := framework.NewDefaultFramework(initialDir + "/bin")
tempDir, err := framework.CopyToTempDir("tests/up/testdata/microsandbox")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir)

_ = f.DevsyProviderDelete(ctx, "microsandbox")
err = f.DevsyProviderAdd(ctx, "microsandbox")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func(cleanupCtx context.Context) {
err := f.DevsyProviderDelete(cleanupCtx, "microsandbox")
framework.ExpectNoError(err)
})

// full up: boots the microVM, streams in the agent, opens the tunnel
err = f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)
ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, tempDir)

// the workspace is reachable over SSH
err = f.DevsySSHEchoTestString(ctx, tempDir)
framework.ExpectNoError(err)

// stop, then bring it back up and confirm it is reachable again
err = f.DevsyWorkspaceStop(ctx, tempDir)
framework.ExpectNoError(err)

err = f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)

err = f.DevsySSHEchoTestString(ctx, tempDir)
framework.ExpectNoError(err)
}, ginkgo.SpecTimeout(framework.TimeoutModerate()))
},
)
4 changes: 4 additions & 0 deletions e2e/tests/up/testdata/microsandbox/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "microsandbox",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
}
4 changes: 2 additions & 2 deletions e2e/tests/up/up_behaviors.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ var _ = ginkgo.Describe("up command behaviors", ginkgo.Label("up-behaviors"), fu
)

ginkgo.It("should accept --update-remote-user-uid=on", func(ctx context.Context) {
if runtime.GOOS != "linux" {
if runtime.GOOS != osLinux {
ginkgo.Skip("updateRemoteUserUID only applies on Linux")
}
_, err := dtc.setupAndUp(ctx, "tests/up/testdata/docker",
Expand All @@ -127,7 +127,7 @@ var _ = ginkgo.Describe("up command behaviors", ginkgo.Label("up-behaviors"), fu
}, ginkgo.SpecTimeout(framework.TimeoutShort()))

ginkgo.It("should accept --update-remote-user-uid=off", func(ctx context.Context) {
if runtime.GOOS != "linux" {
if runtime.GOOS != osLinux {
ginkgo.Skip("updateRemoteUserUID only applies on Linux")
}
tempDir, err := dtc.setupAndUp(ctx, "tests/up/testdata/docker",
Expand Down
20 changes: 20 additions & 0 deletions pkg/agent/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,33 @@ func NewBinaryManager(downloadURL string) (*BinaryManager, error) {

return &BinaryManager{
sources: []BinarySource{
&EnvPathSource{EnvVar: config.EnvAgentBinary},
&InjectSource{},
&FileCacheSource{Cache: cache, ExpectedVersion: expectedVersion},
&HTTPDownloadSource{BaseURL: downloadURL, Cache: cache, Version: expectedVersion},
},
}, nil
}

// EnvPathSource injects an agent binary from a local path named by an
// environment variable, taking precedence over the built-in sources so an
// offline host can supply the Linux binary. The operator must match its arch.
type EnvPathSource struct {
EnvVar string
}

func (s *EnvPathSource) GetBinary(_ context.Context, _ string) (io.ReadCloser, error) {
path := strings.TrimSpace(os.Getenv(s.EnvVar))
if path == "" {
return nil, fmt.Errorf("no agent binary path set in %s", s.EnvVar)
}
return os.Open(path) // #nosec G304 G703 -- operator-provided local agent binary
}

func (s *EnvPathSource) SourceName() string {
return "local path override (" + s.EnvVar + ")"
}

func versionFromDownloadURL(downloadURL string) string {
parts := strings.Split(strings.TrimRight(downloadURL, "/"), "/")
if len(parts) == 0 {
Expand Down
37 changes: 37 additions & 0 deletions pkg/agent/binary_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package agent

import (
"context"
"io"
"os"
"path/filepath"
"testing"
)

func TestEnvPathSourceUsesLocalBinary(t *testing.T) {
dir := t.TempDir()
binPath := filepath.Join(dir, "devsy-linux-arm64")
if err := os.WriteFile(binPath, []byte("agent-bytes"), 0o755); err != nil { //nolint:gosec
t.Fatal(err)
}
t.Setenv("DEVSY_TEST_AGENT_BIN", binPath)

src := &EnvPathSource{EnvVar: "DEVSY_TEST_AGENT_BIN"}
rc, err := src.GetBinary(context.Background(), "arm64")
if err != nil {
t.Fatalf("GetBinary: %v", err)
}
defer func() { _ = rc.Close() }()

data, _ := io.ReadAll(rc)
if string(data) != "agent-bytes" {
t.Errorf("got %q, want the local binary contents", data)
}
}

func TestEnvPathSourceUnsetFallsThrough(t *testing.T) {
src := &EnvPathSource{EnvVar: "DEVSY_TEST_AGENT_BIN_UNSET"}
if _, err := src.GetBinary(context.Background(), "arm64"); err == nil {
t.Fatal("expected an error when the env var is unset so later sources are tried")
}
}
9 changes: 9 additions & 0 deletions pkg/agent/delivery/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ func NewAgentDelivery(opts FactoryOptions) AgentDelivery {
log.Debugf("using shell-based delivery for apple driver")
return &LegacyShellDelivery{ExecFunc: opts.ExecFunc, DownloadURL: ""}

case driverType == provider.MicrosandboxDriver:
// Stream the agent binary over the SDK's guest exec (as kubernetes does);
// fall back to shell delivery when the driver exposes no argv exec.
if opts.PodExec == nil {
return legacyShellDelivery(opts, "microsandbox argv exec unavailable")
}
log.Debugf("using stream delivery (exec stream) for microsandbox")
return &KubernetesDelivery{Exec: opts.PodExec}

case opts.IsRemoteDocker:
log.Debugf("using remote docker delivery (docker cp)")
return remoteDockerDelivery(opts)
Expand Down
32 changes: 32 additions & 0 deletions pkg/agent/delivery/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ func TestNewAgentDelivery_KubernetesDriver_Native(t *testing.T) {
assert.Equal(t, PhasePostStart, d.Phase())
}

func TestNewAgentDelivery_MicrosandboxUsesStreamDelivery(t *testing.T) {
podExec := func(_ context.Context, _ []string, _ driver.Streams) error {
return nil
}

opts := FactoryOptions{
WorkspaceConfig: &provider.AgentWorkspaceInfo{
Agent: provider.ProviderAgentConfig{
Driver: provider.MicrosandboxDriver,
},
},
PodExec: podExec,
}

d := NewAgentDelivery(opts)
native, ok := d.(*KubernetesDelivery)
require.True(t, ok)
assert.NotNil(t, native.Exec)
}

func TestNewAgentDelivery_KubernetesDriver_FallsBackWhenNoPodExec(t *testing.T) {
execFn := func(ctx context.Context, cmd string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
return nil
Expand Down Expand Up @@ -168,3 +188,15 @@ func TestNewAgentDelivery_AppleUsesShellDelivery(t *testing.T) {
t.Fatalf("apple driver must use shell delivery, got %T", d)
}
}

func TestNewAgentDelivery_MicrosandboxUsesShellDelivery(t *testing.T) {
opts := FactoryOptions{
WorkspaceConfig: &provider.AgentWorkspaceInfo{
Agent: provider.ProviderAgentConfig{Driver: provider.MicrosandboxDriver},
},
}
d := NewAgentDelivery(opts)
if _, ok := d.(*LegacyShellDelivery); !ok {
t.Fatalf("microsandbox driver must use shell delivery, got %T", d)
}
}
5 changes: 5 additions & 0 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const (
// EnvHelperImage overrides the helper image for agent volume operations.
EnvHelperImage = "DEVSY_HELPER_IMAGE"

// EnvAgentBinary points to a local agent binary (matching the workspace's
// Linux architecture) to inject instead of downloading. Enables offline and
// air-gapped delivery on hosts that cannot supply the binary directly.
EnvAgentBinary = "DEVSY_AGENT_BINARY"

// EnvAgentPreferDownload forces agent binary download even if a local copy exists.
EnvAgentPreferDownload = "DEVSY_AGENT_PREFER_DOWNLOAD"

Expand Down
6 changes: 6 additions & 0 deletions pkg/credentials/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ type credentialsHandlerFunc func(

func newCredentialsHandler(ctx context.Context, client CredentialsClient) http.Handler {
routes := map[string]credentialsHandlerFunc{
// Root is a readiness probe (see waitForServer); it must return 200 so the
// server is detected as up. Unknown paths still 404 below.
"/": func(_ context.Context, writer http.ResponseWriter, _ *http.Request, _ CredentialsClient) error {
writer.WriteHeader(http.StatusOK)
return nil
},
"/git-credentials": handleGitCredentialsRequest,
"/docker-credentials": handleDockerCredentialsRequest,
"/git-ssh-signature": handleGitSSHSignatureRequest,
Expand Down
8 changes: 6 additions & 2 deletions pkg/driver/drivercreate/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/devsy-org/devsy/pkg/driver/custom"
"github.com/devsy-org/devsy/pkg/driver/docker"
"github.com/devsy-org/devsy/pkg/driver/kubernetes"
"github.com/devsy-org/devsy/pkg/driver/microsandbox"
provider2 "github.com/devsy-org/devsy/pkg/provider"
)

Expand All @@ -26,9 +27,12 @@ func NewDriver(
return kubernetes.NewKubernetesDriver(workspaceInfo)
case provider2.AppleDriver:
return apple.NewAppleDriver(ctx, workspaceInfo)
case provider2.MicrosandboxDriver:
return microsandbox.NewMicrosandboxDriver(ctx, workspaceInfo)
}

return nil, fmt.Errorf("unrecognized driver %q, possible values are %s, %s, %s or %s",
return nil, fmt.Errorf(
"unrecognized driver %q, possible values are %s, %s, %s, %s or %s",
driver, provider2.DockerDriver, provider2.CustomDriver, provider2.KubernetesDriver,
provider2.AppleDriver)
provider2.AppleDriver, provider2.MicrosandboxDriver)
}
Loading
Loading