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
9 changes: 9 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,15 @@ jobs:
test-timeout: 1200s
job-timeout-minutes: 25

- label: up-provider-microsandbox-storage
runner: ubuntu-latest
free-disk-space: false
install-kind: false
requires-secret: false
install-microsandbox: true
test-timeout: 1200s
job-timeout-minutes: 25

# Snapshot tests

- label: snapshot
Expand Down
204 changes: 204 additions & 0 deletions e2e/tests/up/provider_microsandbox_storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package up

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"

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

const microsandboxStorageMarker = "storage-host-marker"

type microsandboxStorageConfig struct {
Image microsandboxStorageImage `json:"image"`
}

type microsandboxStorageImage struct {
OCI *microsandboxStorageOCI `json:"Oci"`
}

type microsandboxStorageOCI struct {
RootDisk *microsandboxStorageDisk `json:"root_disk"`
}

type microsandboxStorageDisk struct {
Kind string `json:"kind"`
SizeMiB uint64 `json:"size_mib"`
}

var _ = ginkgo.Describe("microsandbox storage parity",
ginkgo.Label("up-provider-microsandbox-storage"), func() {
for _, provider := range []struct{ name, source string }{
{"builtin-storage", microsandboxResourceBuiltin},
{"external-storage", microsandboxExternalProvider},
} {
ginkgo.Context(provider.name, func() {
var f *framework.Framework
var workspace string

ginkgo.BeforeEach(func(ctx context.Context) {
skipIfNoMicrosandbox(ctx)
ginkgo.GinkgoT().Setenv("DEVSY_HOME", ginkgo.GinkgoT().TempDir())
ginkgo.GinkgoT().Setenv("DEVSY_CONFIG", "")
initialDir, err := os.Getwd()
framework.ExpectNoError(err)
f = framework.NewDefaultFramework(filepath.Join(initialDir, "bin"))
workspace, err = framework.CreateTempDir()
framework.ExpectNoError(err)
ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, workspace)
framework.ExpectNoError(
f.DevsyProviderAdd(ctx, provider.source, "--name", provider.name),
)
ginkgo.DeferCleanup(func(cleanupCtx context.Context) error {
cleanupCtx, cancel := context.WithTimeout(
context.WithoutCancel(cleanupCtx),
time.Minute,
)
defer cancel()
return f.DevsyProviderDelete(cleanupCtx, provider.name)
})
ginkgo.DeferCleanup(f.CleanupWorkspace, workspace)
})

ginkgo.DescribeTable(
"sizes and preserves the managed root until recreation",
func(ctx context.Context, options []string, storage string, sizeMiB uint64) {
framework.ExpectNoError(f.DevsyProviderUse(ctx, provider.name, options...))
var requirements *config.HostRequirements
if storage != "" {
requirements = &config.HostRequirements{Storage: storage}
}
writeMicrosandboxResourceConfig(workspace, requirements)
framework.ExpectNoError(os.WriteFile(
filepath.Join(
workspace,
microsandboxStorageMarker,
),
[]byte("host-preserved\n"),
0o600,
))
framework.ExpectNoError(
f.DevsyUp(ctx, workspace, "--devcontainer", ".devcontainer.json"),
)
sandbox := microsandboxResourceSandbox(ctx, f, workspace)
assertMicrosandboxStorage(ctx, f, workspace, sizeMiB)
assertMicrosandboxStorageWorkspace(ctx, f, workspace)
createdAt := microsandboxCreationTime(ctx, sandbox)
bootID := microsandboxStorageBootID(ctx, f, workspace)
out, err := f.DevsySSHOnce(ctx, workspace,
"printf 'root-preserved\\n' > /root/storage-parity-marker")
framework.ExpectNoError(err, out)

framework.ExpectNoError(f.DevsyWorkspaceStop(ctx, workspace))
framework.ExpectNoError(f.DevsyUp(ctx, workspace))
gomega.Expect(microsandboxCreationTime(ctx, sandbox)).
To(gomega.Equal(createdAt))
restartedBootID := microsandboxStorageBootID(ctx, f, workspace)
gomega.Expect(restartedBootID).NotTo(gomega.Equal(bootID))
out, err = f.DevsySSHOnce(ctx, workspace, "cat /root/storage-parity-marker")
framework.ExpectNoError(err)
gomega.Expect(out).To(gomega.Equal("root-preserved\n"))
assertMicrosandboxStorage(ctx, f, workspace, sizeMiB)
assertMicrosandboxStorageWorkspace(ctx, f, workspace)

framework.ExpectNoError(f.DevsyUpRecreate(ctx, workspace))
gomega.Expect(microsandboxCreationTime(ctx, sandbox)).
NotTo(gomega.Equal(createdAt))
gomega.Expect(microsandboxStorageBootID(ctx, f, workspace)).
NotTo(gomega.Equal(restartedBootID))
out, err = f.DevsySSHOnce(
ctx,
workspace,
"test ! -e /root/storage-parity-marker",
)
framework.ExpectNoError(err, out)
assertMicrosandboxStorage(ctx, f, workspace, sizeMiB)
assertMicrosandboxStorageWorkspace(ctx, f, workspace)
},
ginkgo.Entry(
"runtime default",
nil,
"",
uint64(4096),
ginkgo.SpecTimeout(framework.TimeoutLong()),
),
ginkgo.Entry("provider overrides host requirements",
[]string{"--option", "MICROSANDBOX_STORAGE=2"}, "3gb", uint64(2048),
ginkgo.SpecTimeout(framework.TimeoutLong())),
ginkgo.Entry("zero provider storage rounds host requirements up to GiB",
[]string{"--option", "MICROSANDBOX_STORAGE=0"}, "1537mb", uint64(2048),
ginkgo.SpecTimeout(framework.TimeoutLong())),
)
})
}
})

func assertMicrosandboxStorage(
ctx context.Context,
f *framework.Framework,
workspace string,
sizeMiB uint64,
) {
sandbox := microsandboxResourceSandbox(ctx, f, workspace)
out, err := microsandboxResourceCommand(ctx, "inspect", sandbox, "--format", "json")
framework.ExpectNoError(err, string(out))
var inspection struct {
ActiveConfig *microsandboxStorageConfig `json:"active_config"`
}
framework.ExpectNoError(json.Unmarshal(out, &inspection), string(out))
gomega.Expect(inspection.ActiveConfig).NotTo(gomega.BeNil())
gomega.Expect(inspection.ActiveConfig.Image.OCI).NotTo(gomega.BeNil())
rootDisk := inspection.ActiveConfig.Image.OCI.RootDisk
gomega.Expect(rootDisk).NotTo(gomega.BeNil())
gomega.Expect(rootDisk.Kind).To(gomega.Equal("managed"))
gomega.Expect(rootDisk.SizeMiB).To(gomega.Equal(sizeMiB))

capacity, err := f.DevsySSHOnce(ctx, workspace, "stat -f -c '%S %b' /")
framework.ExpectNoError(err)
var blockSize, blocks uint64
n, err := fmt.Sscanf(strings.TrimSpace(capacity), "%d %d", &blockSize, &blocks)
framework.ExpectNoError(err)
gomega.Expect(n).To(gomega.Equal(2))
gomega.Expect(blockSize).To(gomega.BeNumerically(">", 0))
totalBytes := blockSize * blocks
expectedBytes := sizeMiB * 1024 * 1024
// Ext4 metadata reduces statfs total blocks; the 2 GiB and 4 GiB ranges remain disjoint.
gomega.Expect(totalBytes).To(gomega.BeNumerically(">=", expectedBytes*9/10))
gomega.Expect(totalBytes).To(gomega.BeNumerically("<=", expectedBytes))
}

func assertMicrosandboxStorageWorkspace(
ctx context.Context,
f *framework.Framework,
workspace string,
) {
data, err := os.ReadFile(
filepath.Join(workspace, microsandboxStorageMarker),
) // #nosec G304 -- test-owned workspace
framework.ExpectNoError(err)
gomega.Expect(string(data)).To(gomega.Equal("host-preserved\n"))
guestMarker := filepath.Join("/workspaces", filepath.Base(workspace), microsandboxStorageMarker)
out, err := f.DevsySSHOnce(ctx, workspace, "cat "+guestMarker)
framework.ExpectNoError(err)
gomega.Expect(out).To(gomega.Equal("host-preserved\n"))
}

func microsandboxStorageBootID(
ctx context.Context,
f *framework.Framework,
workspace string,
) string {
out, err := f.DevsySSHOnce(ctx, workspace, "cat /proc/sys/kernel/random/boot_id")
framework.ExpectNoError(err)
bootID := strings.TrimSpace(out)
gomega.Expect(bootID).To(gomega.MatchRegexp(`^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$`))
return bootID
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,12 @@ The ceiling scenario uses `msb modify` on the test-owned VM to grow CPU and memo
DEVSY_REQUIRE_MICROSANDBOX=true task cli:test:e2e:suite -- up-provider-microsandbox-resources
```

These scenarios do not establish complete parity. Storage capacity, ephemeral roots, egress denial, prebuilds, dockerless operation, logs, cancellation, and runtime compatibility failures still require coverage before replacing the built-in provider. Green lifecycle, image, mount, and resource checks alone do not authorize that cutover.
The `up-provider-microsandbox-storage` label runs three persistent-root scenarios against each provider: the runtime's default capacity, an explicit provider storage option overriding `hostRequirements.storage`, and a zero provider option falling back to a fractional-GiB host requirement rounded up to whole GiB. It checks active root-disk configuration and guest filesystem capacity, allowing for filesystem metadata instead of comparing free space with the raw disk size.

Each scenario verifies that a VM-local file survives stop/start and disappears after recreation, while a host workspace file persists through both operations. Each scenario has a ten-minute deadline, with a 20-minute CI test deadline and a 25-minute job deadline. This suite uses the same runtime and virtualization prerequisites as the lifecycle suite:

```sh
DEVSY_REQUIRE_MICROSANDBOX=true task cli:test:e2e:suite -- up-provider-microsandbox-storage
```

These scenarios do not establish complete parity. Ephemeral roots, egress denial, prebuilds, dockerless operation, logs, cancellation, and runtime compatibility failures still require coverage before replacing the built-in provider. Green lifecycle, image, mount, resource, and persistent-storage checks alone do not authorize that cutover.
Loading