Skip to content

Commit af0530f

Browse files
e2bclaude
authored andcommitted
feat: add TargetArch() utility and arch-aware path resolution
- Add TargetArch() to shared/utils that reads TARGET_ARCH env var with alias normalization (x86_64→amd64, aarch64→arm64), defaulting to runtime.GOARCH - Update Firecracker and kernel path resolution to prefer arch-prefixed layout ({version}/{arch}/binary) with legacy flat fallback - Change OCI DefaultPlatform from hardcoded amd64 var to function using TargetArch() - Add comprehensive tests for path resolution and TargetArch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ac189cc commit af0530f

File tree

6 files changed

+233
-7
lines changed

6 files changed

+233
-7
lines changed

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)

packages/shared/pkg/utils/env.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,35 @@ package utils
33
import (
44
"fmt"
55
"os"
6+
"runtime"
67
"strings"
78
)
89

10+
// archAliases normalizes common architecture names to Go convention.
11+
var archAliases = map[string]string{
12+
"amd64": "amd64",
13+
"x86_64": "amd64",
14+
"arm64": "arm64",
15+
"aarch64": "arm64",
16+
}
17+
18+
// TargetArch returns the target architecture for binary paths and OCI platform.
19+
// If TARGET_ARCH is set, it is normalized to Go convention ("amd64" or "arm64");
20+
// otherwise defaults to the host architecture (runtime.GOARCH).
21+
func TargetArch() string {
22+
if arch := os.Getenv("TARGET_ARCH"); arch != "" {
23+
if normalized, ok := archAliases[arch]; ok {
24+
return normalized
25+
}
26+
27+
fmt.Fprintf(os.Stderr, "WARNING: unrecognized TARGET_ARCH=%q, falling back to %s\n", arch, runtime.GOARCH)
28+
29+
return runtime.GOARCH
30+
}
31+
32+
return runtime.GOARCH
33+
}
34+
935
// RequiredEnv returns the value of the environment variable for key if it is set, non-empty and not only whitespace.
1036
// It panics otherwise.
1137
//
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package utils
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestTargetArch_DefaultsToHostArch(t *testing.T) {
11+
t.Setenv("TARGET_ARCH", "")
12+
13+
result := TargetArch()
14+
15+
assert.Equal(t, runtime.GOARCH, result)
16+
}
17+
18+
func TestTargetArch_RespectsValidOverride(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
arch string
22+
expected string
23+
}{
24+
{name: "amd64", arch: "amd64", expected: "amd64"},
25+
{name: "arm64", arch: "arm64", expected: "arm64"},
26+
}
27+
28+
for _, tt := range tests {
29+
t.Run(tt.name, func(t *testing.T) {
30+
t.Setenv("TARGET_ARCH", tt.arch)
31+
32+
result := TargetArch()
33+
34+
assert.Equal(t, tt.expected, result)
35+
})
36+
}
37+
}
38+
39+
func TestTargetArch_NormalizesAliases(t *testing.T) {
40+
tests := []struct {
41+
name string
42+
arch string
43+
expected string
44+
}{
45+
{name: "x86_64 → amd64", arch: "x86_64", expected: "amd64"},
46+
{name: "aarch64 → arm64", arch: "aarch64", expected: "arm64"},
47+
}
48+
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
t.Setenv("TARGET_ARCH", tt.arch)
52+
53+
result := TargetArch()
54+
55+
assert.Equal(t, tt.expected, result)
56+
})
57+
}
58+
}
59+
60+
func TestTargetArch_FallsBackOnUnknown(t *testing.T) {
61+
t.Setenv("TARGET_ARCH", "mips")
62+
63+
result := TargetArch()
64+
65+
assert.Equal(t, runtime.GOARCH, result)
66+
}

0 commit comments

Comments
 (0)