Skip to content
Closed
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
17 changes: 16 additions & 1 deletion packages/envd/internal/services/process/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ func currentNice() int {
return 20 - prio
}

// findBinary returns the first existing path from candidates, falling back to
// the bare name (relying on PATH lookup at exec time) if none exist on disk.
func findBinary(name string, candidates ...string) string {
for _, p := range candidates {
if _, err := os.Stat(p); err == nil {
return p
}
}
return name
}

func New(
ctx context.Context,
user *user.User,
Expand All @@ -102,8 +113,12 @@ func New(
userCmd := strings.Join(append([]string{req.GetProcess().GetCmd()}, req.GetProcess().GetArgs()...), " ")

// Wrap in a shell that resets oom_score_adj, ioprio (ionice best-effort/4), and nice.
// Use PATH-based lookup for ionice/nice to support Alpine (busybox applets at /bin/)
// in addition to standard distros (binaries at /usr/bin/).
niceDelta := defaultNice - currentNice()
oomWrapperScript := fmt.Sprintf(`echo %d > /proc/$$/oom_score_adj && exec /usr/bin/ionice -c 2 -n 4 /usr/bin/nice -n %d "${@}"`, defaultOomScore, niceDelta)
ionicePath := findBinary("ionice", "/usr/bin/ionice", "/bin/ionice")
nicePath := findBinary("nice", "/usr/bin/nice", "/bin/nice")
oomWrapperScript := fmt.Sprintf(`echo %d > /proc/$$/oom_score_adj && exec %s -c 2 -n 4 %s -n %d "${@}"`, defaultOomScore, ionicePath, nicePath, niceDelta)
wrapperArgs := append([]string{"-c", oomWrapperScript, "--", req.GetProcess().GetCmd()}, req.GetProcess().GetArgs()...)
cmd := exec.CommandContext(ctx, "/bin/sh", wrapperArgs...)

Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,4 @@ Automatically set in local mode. Set before running to override:

## Limitations

- Custom template builds require Debian/Ubuntu-based base images (images that provide the `apt` package manager). Non-Debian images such as Alpine, CentOS/RHEL, or other distributions without `apt` are not supported and will fail during the template build/provisioning process. The provisioning scripts used during template build call `apt` and expect Debian-specific package names and file locations.
- Custom template builds now support mainstream Linux distributions, including Debian/Ubuntu, Alpine, CentOS/RHEL. The provisioning logic automatically adapts to the native package manager (apt, apk, dnf/yum) and distro-specific package naming rules, file paths for each target OS during template building and provisioning.
3 changes: 3 additions & 0 deletions packages/orchestrator/pkg/sandbox/fc/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ func (p *Process) Create(
delete(args, "quiet")
args["console"] = "ttyS0"
args["loglevel"] = "5" // KERN_NOTICE
// Add systemd debug logging for troubleshooting
args["systemd.log_level"] = "debug"
args["systemd.log_target"] = "console"
}

kernelArgs := args.String()
Expand Down
43 changes: 37 additions & 6 deletions packages/orchestrator/pkg/template/build/commands/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,16 @@ func (u *User) Execute(

// Only create user if it doesn't exist
if !userExists {
// Try Debian-style adduser first, fall back to useradd for RHEL/CentOS/Alpine
createUserCmd := buildCreateUserCmd(userArg)
err = sandboxtools.RunCommandWithLogger(
ctx,
proxy,
logger,
lvl,
prefix,
sandboxID,
fmt.Sprintf("adduser --disabled-password --gecos \"\" %s", userArg),
createUserCmd,
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand Down Expand Up @@ -91,15 +93,15 @@ func addToSudoers(
cmdMetadata metadata.Context,
userArg string,
) (metadata.Context, error) {
// Add user to sudo group
// Add user to sudo/wheel group (sudo for Debian/Ubuntu, wheel for RHEL/CentOS/Alpine)
err := sandboxtools.RunCommandWithLogger(
ctx,
proxy,
logger,
lvl,
prefix,
sandboxID,
fmt.Sprintf("usermod -aG sudo %s", userArg),
buildAddToGroupCmd(userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand All @@ -109,15 +111,15 @@ func addToSudoers(
return metadata.Context{}, fmt.Errorf("failed to add user to sudo group: %w", err)
}

// Remove password
// Remove password (passwd may not exist on minimal images)
err = sandboxtools.RunCommandWithLogger(
ctx,
proxy,
logger,
lvl,
prefix,
sandboxID,
fmt.Sprintf("passwd -d %s", userArg),
buildRemovePasswordCmd(userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand All @@ -135,7 +137,7 @@ func addToSudoers(
lvl,
prefix,
sandboxID,
fmt.Sprintf("grep -q '^%s ALL=(ALL:ALL) NOPASSWD: ALL' /etc/sudoers || echo '%s ALL=(ALL:ALL) NOPASSWD: ALL' >>/etc/sudoers", userArg, userArg),
buildSudoersCmd(userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand Down Expand Up @@ -172,3 +174,32 @@ func saveUserMeta(

return cmdMetadata, err
}

// buildCreateUserCmd returns the shell command that creates a user, trying
// Debian-style adduser first, then useradd (RHEL/CentOS), then Alpine adduser.
func buildCreateUserCmd(username string) string {
return fmt.Sprintf(
"adduser --disabled-password --gecos \"\" %s 2>/dev/null || useradd -m %s 2>/dev/null || adduser -D %s",
username, username, username,
)
}

// buildAddToGroupCmd returns the shell command that adds a user to the sudo or
// wheel group, depending on the distro.
func buildAddToGroupCmd(username string) string {
return fmt.Sprintf("usermod -aG sudo %s 2>/dev/null || usermod -aG wheel %s 2>/dev/null || true", username, username)
}

// buildRemovePasswordCmd returns the shell command that removes a user's password.
func buildRemovePasswordCmd(username string) string {
return fmt.Sprintf("passwd -d %s 2>/dev/null || true", username)
}

// buildSudoersCmd returns the shell command that appends a NOPASSWD sudoers
// entry for the given user if it is not already present.
func buildSudoersCmd(username string) string {
return fmt.Sprintf(
"touch /etc/sudoers && (grep -q '^%s ALL=(ALL:ALL) NOPASSWD: ALL' /etc/sudoers || echo '%s ALL=(ALL:ALL) NOPASSWD: ALL' >>/etc/sudoers)",
username, username,
)
}
154 changes: 154 additions & 0 deletions packages/orchestrator/pkg/template/build/commands/user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//go:build linux

package commands

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// ─── buildCreateUserCmd ───────────────────────────────────────────────────────

func TestBuildCreateUserCmd_ContainsUsername(t *testing.T) {
t.Parallel()
cmd := buildCreateUserCmd("alice")
assert.Contains(t, cmd, "alice", "command should reference the username")
}

func TestBuildCreateUserCmd_DebianStyleFirst(t *testing.T) {
t.Parallel()
cmd := buildCreateUserCmd("alice")
// adduser (Debian) must appear before useradd (RHEL) in the fallback chain.
debianIdx := strings.Index(cmd, "adduser --disabled-password")
rhelIdx := strings.Index(cmd, "useradd -m")
assert.Greater(t, debianIdx, -1, "Debian-style adduser should be present")
assert.Greater(t, rhelIdx, -1, "useradd should be present as fallback")
assert.Less(t, debianIdx, rhelIdx, "Debian-style adduser should come before useradd")
}

func TestBuildCreateUserCmd_AlpineFallback(t *testing.T) {
t.Parallel()
cmd := buildCreateUserCmd("alice")
// Alpine's adduser -D must be the last fallback.
assert.Contains(t, cmd, "adduser -D alice", "Alpine adduser -D should be present")
alpineIdx := strings.Index(cmd, "adduser -D")
rhelIdx := strings.Index(cmd, "useradd -m")
assert.Greater(t, alpineIdx, rhelIdx, "Alpine adduser -D should come after useradd")
}

func TestBuildCreateUserCmd_SuppressesStderr(t *testing.T) {
t.Parallel()
cmd := buildCreateUserCmd("bob")
// Each alternative should redirect stderr to /dev/null so that the
// fallback chain works correctly (non-zero exit triggers the next ||).
assert.Contains(t, cmd, "2>/dev/null", "stderr should be suppressed")
}

func TestBuildCreateUserCmd_SpecialUsername(t *testing.T) {
t.Parallel()
cmd := buildCreateUserCmd("www-data")
assert.Contains(t, cmd, "www-data")
}

// ─── buildAddToGroupCmd ───────────────────────────────────────────────────────

func TestBuildAddToGroupCmd_ContainsSudoGroup(t *testing.T) {
t.Parallel()
cmd := buildAddToGroupCmd("alice")
assert.Contains(t, cmd, "sudo", "should add to sudo group (Debian/Ubuntu)")
}

func TestBuildAddToGroupCmd_ContainsWheelGroup(t *testing.T) {
t.Parallel()
cmd := buildAddToGroupCmd("alice")
assert.Contains(t, cmd, "wheel", "should add to wheel group (RHEL/CentOS/Alpine)")
}

func TestBuildAddToGroupCmd_SudoBeforeWheel(t *testing.T) {
t.Parallel()
cmd := buildAddToGroupCmd("alice")
sudoIdx := strings.Index(cmd, "sudo")
wheelIdx := strings.Index(cmd, "wheel")
assert.Less(t, sudoIdx, wheelIdx, "sudo group should be tried before wheel")
}

func TestBuildAddToGroupCmd_EndsWithTrue(t *testing.T) {
t.Parallel()
cmd := buildAddToGroupCmd("alice")
// The command must end with `|| true` so it never fails even on distros
// that have neither sudo nor wheel groups.
assert.True(t, strings.HasSuffix(strings.TrimSpace(cmd), "|| true"),
"command should end with '|| true' to be non-fatal")
}

func TestBuildAddToGroupCmd_ContainsUsername(t *testing.T) {
t.Parallel()
cmd := buildAddToGroupCmd("charlie")
assert.Contains(t, cmd, "charlie")
}

// ─── buildRemovePasswordCmd ───────────────────────────────────────────────────

func TestBuildRemovePasswordCmd_ContainsUsername(t *testing.T) {
t.Parallel()
cmd := buildRemovePasswordCmd("alice")
assert.Contains(t, cmd, "alice")
}

func TestBuildRemovePasswordCmd_UsesPasswdD(t *testing.T) {
t.Parallel()
cmd := buildRemovePasswordCmd("alice")
assert.Contains(t, cmd, "passwd -d", "should use passwd -d to remove password")
}

func TestBuildRemovePasswordCmd_NonFatal(t *testing.T) {
t.Parallel()
cmd := buildRemovePasswordCmd("alice")
// Must be non-fatal because passwd may not exist on minimal images.
assert.True(t, strings.HasSuffix(strings.TrimSpace(cmd), "|| true"),
"command should end with '|| true' to be non-fatal")
}

// ─── buildSudoersCmd ─────────────────────────────────────────────────────────

func TestBuildSudoersCmd_ContainsUsername(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
assert.Contains(t, cmd, "alice")
}

func TestBuildSudoersCmd_ContainsNopasswd(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
assert.Contains(t, cmd, "NOPASSWD: ALL", "sudoers entry should grant passwordless sudo")
}

func TestBuildSudoersCmd_IdempotentViaGrep(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
// The command must check for an existing entry before appending.
assert.Contains(t, cmd, "grep -q", "should check for existing entry before appending")
}

func TestBuildSudoersCmd_TouchesSudoers(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
// Must ensure /etc/sudoers exists even on minimal images.
assert.Contains(t, cmd, "touch /etc/sudoers")
}

func TestBuildSudoersCmd_AppendsEntry(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
assert.Contains(t, cmd, ">>", "should append to sudoers file")
assert.Contains(t, cmd, "/etc/sudoers")
}

func TestBuildSudoersCmd_FullEntryFormat(t *testing.T) {
t.Parallel()
cmd := buildSudoersCmd("alice")
// Verify the exact sudoers rule format.
assert.Contains(t, cmd, "alice ALL=(ALL:ALL) NOPASSWD: ALL")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{- /*gotype:github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/core/rootfs.templateModel*/ -}}
{{ .WriteFile "/etc/systemd/system-preset/80-envd.preset" 0o644 }}
enable envd.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{- /*gotype:github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/core/rootfs.templateModel*/ -}}
{{ .WriteFile "/etc/init.d/envd" 0o755 }}
#!/sbin/openrc-run

description="Env Daemon Service"

command="/usr/bin/envd"
command_background=true
pidfile="/run/envd.pid"

# Start as early as possible
start_stop_daemon_args="--nicelevel -20"

depend() {
need localmount
after bootmisc
}

start_pre() {
# Seed the tmpfs from the tar packed as the build's last guest step
if ! mountpoint -q /etc/ssl/certs 2>/dev/null; then
mkdir -p /run/e2b/certs
if tar -C /run/e2b/certs -xf /usr/local/share/e2b/ssl-certs.tar 2>/dev/null || \
cp -a /etc/ssl/certs/. /run/e2b/certs/ 2>/dev/null; then
mount --bind /run/e2b/certs /etc/ssl/certs
fi
[ -s /etc/ssl/certs/ca-certificates.crt ] || update-ca-certificates 2>/dev/null || true
fi
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ LimitCORE=infinity
# guaranteed present for the sandbox's routable lifetime. The only gap is guest
# units that auto-start and egress over TLS before /init; that is accepted
# (revisit if a template needs boot-time egress).
ExecStartPre=/bin/sh -c 'mountpoint -q /etc/ssl/certs || { mkdir -p /run/e2b/certs && { tar -C /run/e2b/certs -xf /usr/local/share/e2b/ssl-certs.tar 2>/dev/null || cp -a /etc/ssl/certs/. /run/e2b/certs/ 2>/dev/null; }; mount --bind /run/e2b/certs /etc/ssl/certs; } && ([ -s /etc/ssl/certs/ca-certificates.crt ] || update-ca-certificates)'
ExecStartPre=-/bin/sh -c 'mountpoint -q /etc/ssl/certs || { mkdir -p /run/e2b/certs && { tar -C /run/e2b/certs -xf /usr/local/share/e2b/ssl-certs.tar 2>/dev/null || cp -a /etc/ssl/certs/. /run/e2b/certs/ 2>/dev/null; }; mount --bind /run/e2b/certs /etc/ssl/certs; } && ([ -s /etc/ssl/certs/ca-certificates.crt ] || update-ca-certificates 2>/dev/null || update-ca-trust extract 2>/dev/null || true)'
ExecStart=/usr/bin/envd
Nice=-20
IOSchedulingClass=realtime
Expand Down
Loading