Skip to content

Commit 36b1781

Browse files
e2bclaude
authored andcommitted
feat: arch-aware Firecracker and kernel path resolution
- config.go: prefer arch-prefixed paths ({version}/{arch}/binary) with legacy flat path fallback for existing production nodes - create-build: download from GCS with {version}/{arch}/ layout, legacy fallback only for amd64 and only on 404 - OCI: use TargetArch() for container image platform selection - Tests for arch-prefixed vs legacy path precedence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd15e97 commit 36b1781

5 files changed

Lines changed: 238 additions & 26 deletions

File tree

packages/orchestrator/cmd/create-build/main.go

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -408,63 +408,141 @@ func printLocalFileSizes(basePath, buildID string) {
408408
}
409409

410410
func setupKernel(ctx context.Context, dir, version string) error {
411-
dstPath := filepath.Join(dir, version, "vmlinux.bin")
411+
arch := utils.TargetArch()
412+
dstPath := filepath.Join(dir, version, arch, "vmlinux.bin")
413+
412414
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
413415
return fmt.Errorf("mkdir kernel dir: %w", err)
414416
}
415417

416418
if _, err := os.Stat(dstPath); err == nil {
417-
fmt.Printf("✓ Kernel %s exists\n", version)
419+
fmt.Printf("✓ Kernel %s (%s) exists\n", version, arch)
418420

419421
return nil
420422
}
421423

422-
kernelURL, _ := url.JoinPath("https://storage.googleapis.com/e2b-prod-public-builds/kernels/", version, "vmlinux.bin")
423-
fmt.Printf("⬇ Downloading kernel %s...\n", version)
424+
// Try arch-specific URL first: {version}/{arch}/vmlinux.bin
425+
archURL, err := url.JoinPath("https://storage.googleapis.com/e2b-prod-public-builds/kernels/", version, arch, "vmlinux.bin")
426+
if err != nil {
427+
return fmt.Errorf("invalid kernel URL: %w", err)
428+
}
429+
430+
fmt.Printf("⬇ Downloading kernel %s (%s)...\n", version, arch)
431+
432+
if err := download(ctx, archURL, dstPath, 0o644); err == nil {
433+
return nil
434+
} else if !errors.Is(err, errNotFound) {
435+
return fmt.Errorf("failed to download kernel: %w", err)
436+
}
437+
438+
// Legacy URLs are x86_64-only; only fall back for amd64.
439+
if arch != "amd64" {
440+
return fmt.Errorf("kernel %s not found for %s (no legacy fallback for non-amd64)", version, arch)
441+
}
442+
443+
legacyURL, err := url.JoinPath("https://storage.googleapis.com/e2b-prod-public-builds/kernels/", version, "vmlinux.bin")
444+
if err != nil {
445+
return fmt.Errorf("invalid kernel legacy URL: %w", err)
446+
}
447+
448+
fmt.Printf(" %s path not found, trying legacy URL...\n", arch)
424449

425-
return download(ctx, kernelURL, dstPath, 0o644)
450+
return download(ctx, legacyURL, dstPath, 0o644)
426451
}
427452

428453
func setupFC(ctx context.Context, dir, version string) error {
429-
dstPath := filepath.Join(dir, version, "firecracker")
454+
arch := utils.TargetArch()
455+
dstPath := filepath.Join(dir, version, arch, "firecracker")
456+
430457
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
431458
return fmt.Errorf("mkdir firecracker dir: %w", err)
432459
}
433460

434461
if _, err := os.Stat(dstPath); err == nil {
435-
fmt.Printf("✓ Firecracker %s exists\n", version)
462+
fmt.Printf("✓ Firecracker %s (%s) exists\n", version, arch)
436463

437464
return nil
438465
}
439466

440-
fcURL := fmt.Sprintf("https://github.com/e2b-dev/fc-versions/releases/download/%s/firecracker", version)
441-
fmt.Printf("⬇ Downloading Firecracker %s...\n", version)
467+
// Download from GCS bucket with {version}/{arch}/firecracker path
468+
fcURL, err := url.JoinPath("https://storage.googleapis.com/e2b-prod-public-builds/fc-versions/", version, arch, "firecracker")
469+
if err != nil {
470+
return fmt.Errorf("invalid Firecracker URL: %w", err)
471+
}
472+
473+
fmt.Printf("⬇ Downloading Firecracker %s (%s)...\n", version, arch)
474+
475+
if err := download(ctx, fcURL, dstPath, 0o755); err == nil {
476+
return nil
477+
} else if !errors.Is(err, errNotFound) {
478+
return fmt.Errorf("failed to download Firecracker: %w", err)
479+
}
480+
481+
// Legacy URLs are x86_64-only; only fall back for amd64.
482+
if arch != "amd64" {
483+
return fmt.Errorf("firecracker %s not found for %s (no legacy fallback for non-amd64)", version, arch)
484+
}
485+
486+
legacyURL, err := url.JoinPath("https://storage.googleapis.com/e2b-prod-public-builds/fc-versions/", version, "firecracker")
487+
if err != nil {
488+
return fmt.Errorf("invalid Firecracker legacy URL: %w", err)
489+
}
490+
491+
fmt.Printf(" %s path not found, trying legacy URL...\n", arch)
442492

443-
return download(ctx, fcURL, dstPath, 0o755)
493+
return download(ctx, legacyURL, dstPath, 0o755)
444494
}
445495

446-
func download(ctx context.Context, url, path string, perm os.FileMode) error {
447-
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
496+
var errNotFound = errors.New("not found")
497+
498+
func download(ctx context.Context, rawURL, path string, perm os.FileMode) error {
499+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
500+
if err != nil {
501+
return fmt.Errorf("invalid download URL %s: %w", rawURL, err)
502+
}
503+
448504
resp, err := (&http.Client{Timeout: 5 * time.Minute}).Do(req)
449505
if err != nil {
450506
return err
451507
}
452508
defer resp.Body.Close()
453509

510+
if resp.StatusCode == http.StatusNotFound {
511+
return fmt.Errorf("%w: %s", errNotFound, rawURL)
512+
}
454513
if resp.StatusCode != http.StatusOK {
455-
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, url)
514+
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, rawURL)
456515
}
457516

458-
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
517+
// Write to a temporary file and rename atomically to avoid partial files
518+
// on network errors or disk-full conditions.
519+
tmpPath := path + ".tmp"
520+
521+
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
459522
if err != nil {
460523
return err
461524
}
462-
defer f.Close()
463525

464-
_, err = io.Copy(f, resp.Body)
465-
if err == nil {
466-
fmt.Printf("✓ Downloaded %s\n", filepath.Base(path))
526+
if _, err := io.Copy(f, resp.Body); err != nil {
527+
f.Close()
528+
os.Remove(tmpPath)
529+
530+
return err
531+
}
532+
533+
if err := f.Close(); err != nil {
534+
os.Remove(tmpPath)
535+
536+
return err
467537
}
468538

469-
return err
539+
if err := os.Rename(tmpPath, path); err != nil {
540+
os.Remove(tmpPath)
541+
542+
return err
543+
}
544+
545+
fmt.Printf("✓ Downloaded %s\n", filepath.Base(path))
546+
547+
return nil
470548
}

packages/orchestrator/pkg/sandbox/fc/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package fc
22

33
import (
4+
"os"
45
"path/filepath"
56

67
"github.com/e2b-dev/infra/packages/orchestrator/pkg/cfg"
8+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
79
)
810

911
const (
@@ -31,10 +33,25 @@ func (t Config) SandboxKernelDir() string {
3133
}
3234

3335
func (t Config) HostKernelPath(config cfg.BuilderConfig) string {
36+
// Prefer arch-prefixed path ({version}/{arch}/vmlinux.bin) for multi-arch support.
37+
// Fall back to legacy flat path ({version}/vmlinux.bin) for existing production nodes.
38+
archPath := filepath.Join(config.HostKernelsDir, t.KernelVersion, utils.TargetArch(), SandboxKernelFile)
39+
if _, err := os.Stat(archPath); err == nil {
40+
return archPath
41+
}
42+
3443
return filepath.Join(config.HostKernelsDir, t.KernelVersion, SandboxKernelFile)
3544
}
3645

3746
func (t Config) FirecrackerPath(config cfg.BuilderConfig) string {
47+
// Prefer arch-prefixed path ({version}/{arch}/firecracker) for multi-arch support.
48+
// Fall back to legacy flat path ({version}/firecracker) for existing production nodes
49+
// that haven't migrated to the arch-prefixed layout yet.
50+
archPath := filepath.Join(config.FirecrackerVersionsDir, t.FirecrackerVersion, utils.TargetArch(), FirecrackerBinaryName)
51+
if _, err := os.Stat(archPath); err == nil {
52+
return archPath
53+
}
54+
3855
return filepath.Join(config.FirecrackerVersionsDir, t.FirecrackerVersion, FirecrackerBinaryName)
3956
}
4057

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package fc
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/cfg"
12+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
13+
)
14+
15+
func TestFirecrackerPath_ArchPrefixed(t *testing.T) {
16+
t.Parallel()
17+
dir := t.TempDir()
18+
arch := utils.TargetArch()
19+
20+
// Create the arch-prefixed binary
21+
archDir := filepath.Join(dir, "v1.12.0", arch)
22+
require.NoError(t, os.MkdirAll(archDir, 0o755))
23+
require.NoError(t, os.WriteFile(filepath.Join(archDir, "firecracker"), []byte("binary"), 0o755))
24+
25+
config := cfg.BuilderConfig{FirecrackerVersionsDir: dir}
26+
fc := Config{FirecrackerVersion: "v1.12.0"}
27+
28+
result := fc.FirecrackerPath(config)
29+
30+
assert.Equal(t, filepath.Join(dir, "v1.12.0", arch, "firecracker"), result)
31+
}
32+
33+
func TestFirecrackerPath_LegacyFallback(t *testing.T) {
34+
t.Parallel()
35+
dir := t.TempDir()
36+
37+
// Only create the legacy flat binary (no arch subdirectory)
38+
require.NoError(t, os.MkdirAll(filepath.Join(dir, "v1.12.0"), 0o755))
39+
require.NoError(t, os.WriteFile(filepath.Join(dir, "v1.12.0", "firecracker"), []byte("binary"), 0o755))
40+
41+
config := cfg.BuilderConfig{FirecrackerVersionsDir: dir}
42+
fc := Config{FirecrackerVersion: "v1.12.0"}
43+
44+
result := fc.FirecrackerPath(config)
45+
46+
assert.Equal(t, filepath.Join(dir, "v1.12.0", "firecracker"), result)
47+
}
48+
49+
func TestFirecrackerPath_NeitherExists(t *testing.T) {
50+
t.Parallel()
51+
dir := t.TempDir()
52+
53+
// No binary at all — should return legacy flat path
54+
config := cfg.BuilderConfig{FirecrackerVersionsDir: dir}
55+
fc := Config{FirecrackerVersion: "v1.12.0"}
56+
57+
result := fc.FirecrackerPath(config)
58+
59+
assert.Equal(t, filepath.Join(dir, "v1.12.0", "firecracker"), result)
60+
}
61+
62+
func TestHostKernelPath_ArchPrefixed(t *testing.T) {
63+
t.Parallel()
64+
dir := t.TempDir()
65+
arch := utils.TargetArch()
66+
67+
// Create the arch-prefixed kernel
68+
archDir := filepath.Join(dir, "vmlinux-6.1.102", arch)
69+
require.NoError(t, os.MkdirAll(archDir, 0o755))
70+
require.NoError(t, os.WriteFile(filepath.Join(archDir, "vmlinux.bin"), []byte("kernel"), 0o644))
71+
72+
config := cfg.BuilderConfig{HostKernelsDir: dir}
73+
fc := Config{KernelVersion: "vmlinux-6.1.102"}
74+
75+
result := fc.HostKernelPath(config)
76+
77+
assert.Equal(t, filepath.Join(dir, "vmlinux-6.1.102", arch, "vmlinux.bin"), result)
78+
}
79+
80+
func TestHostKernelPath_LegacyFallback(t *testing.T) {
81+
t.Parallel()
82+
dir := t.TempDir()
83+
84+
// Only create the legacy flat kernel
85+
require.NoError(t, os.MkdirAll(filepath.Join(dir, "vmlinux-6.1.102"), 0o755))
86+
require.NoError(t, os.WriteFile(filepath.Join(dir, "vmlinux-6.1.102", "vmlinux.bin"), []byte("kernel"), 0o644))
87+
88+
config := cfg.BuilderConfig{HostKernelsDir: dir}
89+
fc := Config{KernelVersion: "vmlinux-6.1.102"}
90+
91+
result := fc.HostKernelPath(config)
92+
93+
assert.Equal(t, filepath.Join(dir, "vmlinux-6.1.102", "vmlinux.bin"), result)
94+
}
95+
96+
func TestHostKernelPath_PrefersArchOverLegacy(t *testing.T) {
97+
t.Parallel()
98+
dir := t.TempDir()
99+
arch := utils.TargetArch()
100+
101+
// Create BOTH arch-prefixed and legacy flat kernels
102+
require.NoError(t, os.MkdirAll(filepath.Join(dir, "vmlinux-6.1.102", arch), 0o755))
103+
require.NoError(t, os.WriteFile(filepath.Join(dir, "vmlinux-6.1.102", arch, "vmlinux.bin"), []byte("arch-kernel"), 0o644))
104+
require.NoError(t, os.WriteFile(filepath.Join(dir, "vmlinux-6.1.102", "vmlinux.bin"), []byte("legacy-kernel"), 0o644))
105+
106+
config := cfg.BuilderConfig{HostKernelsDir: dir}
107+
fc := Config{KernelVersion: "vmlinux-6.1.102"}
108+
109+
result := fc.HostKernelPath(config)
110+
111+
// Should prefer the arch-prefixed path
112+
assert.Equal(t, filepath.Join(dir, "vmlinux-6.1.102", arch, "vmlinux.bin"), result)
113+
}

packages/orchestrator/pkg/template/build/core/oci/oci.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ func (e *ImageTooLargeError) Error() string {
5656
)
5757
}
5858

59-
var DefaultPlatform = containerregistry.Platform{
60-
OS: "linux",
61-
Architecture: "amd64",
59+
// DefaultPlatform returns the OCI platform for image pulls, respecting TARGET_ARCH.
60+
func DefaultPlatform() containerregistry.Platform {
61+
return containerregistry.Platform{
62+
OS: "linux",
63+
Architecture: utils.TargetArch(),
64+
}
6265
}
6366

6467
// wrapImagePullError converts technical Docker registry errors into user-friendly messages.
@@ -96,7 +99,7 @@ func GetPublicImage(ctx context.Context, dockerhubRepository dockerhub.RemoteRep
9699
return nil, fmt.Errorf("invalid image reference '%s': %w", tag, err)
97100
}
98101

99-
platform := DefaultPlatform
102+
platform := DefaultPlatform()
100103

101104
// When no auth provider is provided and the image is from the default registry
102105
// use docker remote repository proxy with cached images
@@ -149,7 +152,7 @@ func GetImage(ctx context.Context, artifactRegistry artifactsregistry.ArtifactsR
149152
childCtx, childSpan := tracer.Start(ctx, "pull-docker-image")
150153
defer childSpan.End()
151154

152-
platform := DefaultPlatform
155+
platform := DefaultPlatform()
153156

154157
img, err := artifactRegistry.GetImage(childCtx, templateId, buildId, platform)
155158
if err != nil {
@@ -469,7 +472,7 @@ func verifyImagePlatform(img containerregistry.Image, platform containerregistry
469472
return fmt.Errorf("error getting image config file: %w", err)
470473
}
471474
if config.Architecture != platform.Architecture {
472-
return fmt.Errorf("image is not %s", platform.Architecture)
475+
return fmt.Errorf("image architecture %q does not match expected %q", config.Architecture, platform.Architecture)
473476
}
474477

475478
return nil

packages/orchestrator/pkg/template/build/core/oci/oci_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/e2b-dev/infra/packages/shared/pkg/dockerhub"
2727
templatemanager "github.com/e2b-dev/infra/packages/shared/pkg/grpc/template-manager"
2828
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
29+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
2930
)
3031

3132
func createFileTar(t *testing.T, fileName string) *bytes.Buffer {
@@ -213,7 +214,7 @@ func TestGetPublicImageWithGeneralAuth(t *testing.T) {
213214
// Set the config to include the proper platform
214215
configFile, err := testImage.ConfigFile()
215216
require.NoError(t, err)
216-
configFile.Architecture = "amd64"
217+
configFile.Architecture = utils.TargetArch()
217218
configFile.OS = "linux"
218219
testImage, err = mutate.ConfigFile(testImage, configFile)
219220
require.NoError(t, err)

0 commit comments

Comments
 (0)