Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 11 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,19 @@ 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 := fmt.Sprintf(
"adduser --disabled-password --gecos \"\" %s 2>/dev/null || useradd -m %s 2>/dev/null || adduser -D %s",
userArg, userArg, userArg,
)
Comment thread
AdaAibaby marked this conversation as resolved.
Outdated
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 +96,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),
fmt.Sprintf("usermod -aG sudo %s 2>/dev/null || usermod -aG wheel %s 2>/dev/null || true", userArg, userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand All @@ -109,15 +114,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),
fmt.Sprintf("passwd -d %s 2>/dev/null || true", userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand All @@ -135,7 +140,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),
fmt.Sprintf("touch /etc/sudoers && (grep -q '^%s ALL=(ALL:ALL) NOPASSWD: ALL' /etc/sudoers || echo '%s ALL=(ALL:ALL) NOPASSWD: ALL' >>/etc/sudoers)", userArg, userArg),
metadata.Context{
User: "root",
EnvVars: cmdMetadata.EnvVars,
Expand Down
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
100 changes: 100 additions & 0 deletions packages/orchestrator/pkg/template/build/phases/base/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ func (bb *BaseBuilder) buildLayerFromOCI(
return metadata.Template{}, fmt.Errorf("error enlarging disk after provisioning: %w", err)
}

// Fix envd preset files after provisioning
// systemd 219 (CentOS 7) re-creates .wants/ symlinks from preset files on first boot
// when /etc is unpopulated. We need to ensure the preset file exists in /etc/systemd/system-preset/
// which has higher priority than /usr/lib/systemd/system-preset/
err = bb.fixEnvdPresetFiles(ctx, rootfsPath)
if err != nil {
userLogger.Warn(ctx, "Warning: failed to fix envd preset files", zap.Error(err))
// Don't fail the build, just warn
}

// Create sandbox for building template
userLogger.Debug(ctx, "Creating base sandbox template layer")

Expand Down Expand Up @@ -395,3 +405,93 @@ func (bb *BaseBuilder) Layer(
}, nil
}
}

// fixEnvdPresetFiles ensures the envd preset file exists in /etc/systemd/system-preset/
// after provisioning. systemd 219 (CentOS 7) re-creates .wants/ symlinks from preset files
// on first boot when /etc is unpopulated, so we need to ensure the preset file is present.
func (bb *BaseBuilder) fixEnvdPresetFiles(ctx context.Context, rootfsPath string) error {
// Mount the rootfs to access it
mountPoint := filepath.Join(bb.BuilderConfig.TemplatesDir, "preset-fix-mount")
Comment thread
AdaAibaby marked this conversation as resolved.
Outdated
err := os.MkdirAll(mountPoint, 0o755)
if err != nil {
return fmt.Errorf("error creating mount point: %w", err)
}
defer os.RemoveAll(mountPoint)

// Mount the rootfs
err = filesystem.Mount(ctx, rootfsPath, mountPoint)
if err != nil {
return fmt.Errorf("error mounting rootfs: %w", err)
}
defer filesystem.Unmount(ctx, mountPoint)

// Create /etc/systemd/system-preset directory if it doesn't exist
presetDir := filepath.Join(mountPoint, "etc/systemd/system-preset")
err = os.MkdirAll(presetDir, 0o755)
if err != nil {
return fmt.Errorf("error creating preset directory: %w", err)
}

// Write the preset file
presetContent := "enable envd.service\n"
presetFile := filepath.Join(presetDir, "80-envd.preset")
err = os.WriteFile(presetFile, []byte(presetContent), 0o644)
if err != nil {
return fmt.Errorf("error writing preset file: %w", err)
}

// Also ensure /usr/lib/systemd/system-preset/80-envd.preset exists
usrPresetDir := filepath.Join(mountPoint, "usr/lib/systemd/system-preset")
err = os.MkdirAll(usrPresetDir, 0o755)
if err != nil {
return fmt.Errorf("error creating usr preset directory: %w", err)
}

usrPresetFile := filepath.Join(usrPresetDir, "80-envd.preset")
err = os.WriteFile(usrPresetFile, []byte(presetContent), 0o644)
if err != nil {
return fmt.Errorf("error writing usr preset file: %w", err)
}

// Directly re-create the envd.service symlink in multi-user.target.wants
// This is critical for CentOS 7 (systemd 219) where provisioning may have
// removed or overwritten the symlink, and systemd's preset-based re-creation
// on first boot is unreliable.
wantsDir := filepath.Join(mountPoint, "etc/systemd/system/multi-user.target.wants")
err = os.MkdirAll(wantsDir, 0o755)
if err != nil {
return fmt.Errorf("error creating wants directory: %w", err)
}

envdSymlink := filepath.Join(wantsDir, "envd.service")
// Remove existing (possibly broken) symlink
os.Remove(envdSymlink)
// Create symlink pointing to the envd.service unit file
// Try /etc/systemd/system/envd.service first (where rootfs layer puts it)
envdServicePath := "/etc/systemd/system/envd.service"
if _, statErr := os.Lstat(filepath.Join(mountPoint, "etc/systemd/system/envd.service")); statErr != nil {
// Fallback: maybe it's in /usr/lib/systemd/system/
if _, statErr2 := os.Lstat(filepath.Join(mountPoint, "usr/lib/systemd/system/envd.service")); statErr2 == nil {
envdServicePath = "/usr/lib/systemd/system/envd.service"
}
}
err = os.Symlink(envdServicePath, envdSymlink)
if err != nil {
return fmt.Errorf("error creating envd.service symlink: %w", err)
}

// Also re-create chrony.service symlink
chronySymlink := filepath.Join(wantsDir, "chrony.service")
os.Remove(chronySymlink)
chronyServicePath := "/etc/systemd/system/chrony.service"
if _, statErr := os.Lstat(filepath.Join(mountPoint, "etc/systemd/system/chrony.service")); statErr != nil {
if _, statErr2 := os.Lstat(filepath.Join(mountPoint, "usr/lib/systemd/system/chronyd.service")); statErr2 == nil {
chronyServicePath = "/usr/lib/systemd/system/chronyd.service"
} else if _, statErr3 := os.Lstat(filepath.Join(mountPoint, "usr/lib/systemd/system/chrony.service")); statErr3 == nil {
chronyServicePath = "/usr/lib/systemd/system/chrony.service"
}
}
os.Symlink(chronyServicePath, chronySymlink)

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
)

const (
provisionTimeout = 5 * time.Minute
provisionTimeout = 15 * time.Minute
)

//go:embed provision.sh
Expand Down
Loading