Skip to content

feat(orch): distro-aware template provisioning (Fedora/RHEL/Arch base images) - #3381

Closed
tomassrnka wants to merge 20 commits into
mainfrom
feat/multi-distro-templates
Closed

tomassrnka wants to merge 20 commits into
mainfrom
feat/multi-distro-templates

Conversation

@tomassrnka

Copy link
Copy Markdown
Member

What

Template builds are Debian/Ubuntu-only today: the base-phase provision.sh hardcodes apt/dpkg, Debian package names, the /lib/systemd/systemd init path and the chrony unit — so a Fedora / RHEL / CentOS Stream / Rocky / Alma / Arch base image fails opaquely during provisioning (apt-get: command not found).

This makes provisioning distro-aware by keying on the base image's declared /etc/os-release ID — not by probing which package-manager binary happens to exist. Each supported family is a declared Profile in a new phases/base/distro package; provision.sh selects the profile in-guest by $ID via a selector generated from that registry, then uses the profile's package set, package-manager functions, init path, chrony unit, admin group and CA handling. An unsupported distro exits fast with a clear, customer-visible error instead of failing halfway.

Why this shape (not package-manager detection)

Keying on the distro (os-release ID) rather than the package manager (command -v dnf) is deliberate: package-manager detection conflates distros that share a manager but differ in unit names, paths and package names, and it can't represent a non-systemd distro at all. Keeping the axis on the declared distro puts each family's divergences in one declared table and makes the init system a first-class dimension. (Rationale captured in the architecture record as ADR-010; this supersedes the approach in #2941, reusing only its verified package-name/unit/CA mapping.)

Changes

  • new phases/base/distroProfile registry (debian/ubuntu; RHEL family incl. fedora|rhel|centos|rocky|almalinux|ol|amzn; arch) + a generated POSIX-sh selector + unit tests.
  • provision.sh — sources /etc/os-release, runs the selector, then does profile-driven package check/install, init-link, chrony enable, and CA-bundle ensure. Debian package set / query / init path preserved (no behaviour change for existing templates).
  • rootfs.go — drops the static Debian chrony.service autostart symlink; provision.sh now enables the distro-correct unit (chrony vs chronyd).
  • configure.shadduseruseradd (portable across families); admin group sudo || wheel.

Scope

v1 = the systemd family (Debian/Ubuntu unchanged; adds Fedora/RHEL/Arch). Explicitly out of scope / follow-ups:

  • Alpine (OpenRC + musl) — rejected with a clear error; needs a separate OpenRC track.
  • Egress-proxy CA trust on RHEL — envd's cacerts.go hardcodes the Debian bundle path/anchor dir; RHEL's /etc/pki + update-ca-trust path needs profile-fed handling before egress interception works on RHEL. (This PR ensures the trust bundle exists at the expected path; the envd-side per-distro anchor handling is the follow-up.)

Validation

  • go test ./pkg/template/build/phases/base/distro/green (Debian preserved, families differ, selector keys on $E2B_DISTRO_ID with no package-manager probing, unknown distro rejected, RHEL aliases resolve).
  • The rendered provisioning was exercised against real Ubuntu 22.04 / Fedora 40 / Alpine rootfses on a Linux+KVM dev VM: today's apt step gives command not found (exit 127) on Fedora, while the profile-driven path provisions Ubuntu and Fedora cleanly (packages installed, systemd init path present, CA bundle at the envd-expected path); Alpine confirmed it needs the OpenRC path (no systemd init binary).
  • Full in-orchestrator template build + Fedora sandbox boot on an infra dev slot: in progress.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes core template provisioning, guest boot/init wiring, and base-layer cache invalidation across many distro families; regression risk is mitigated by Debian golden tests and fingerprinting, but wrong profile data could still break builds or envd autostart on non-Debian images.

Overview
Template builds no longer assume Debian-only apt provisioning: the base phase selects a declared distro profile from /etc/os-release ID (not package-manager probing), installs family-specific packages, wires the correct init and time-sync units, refreshes CA bundles where needed, and fails fast with a clear error for unsupported images. Debian/Ubuntu behavior is preserved; Fedora/RHEL/Arch gain dnf/pacman paths, RHEL CA linking, and systemd preset fixes so envd still autostarts after first boot.

Boot and runtime paths are broadened for minimal images: provisioning runs through baked busybox without /bin/sh in inittab, shared cert seeding and an OpenRC envd service for Alpine, NixOS as a premade declarative base, portable useradd / sudo vs wheel in build steps, and envd process spawn that skips hardcoded ionice/nice when absent. Base-layer cache hashing now includes the distro contract fingerprint so profile changes cannot reuse stale provisioned bases.

Reviewed by Cursor Bugbot for commit 369027b. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
3526 2 3524 7
View the top 3 failed test(s) by shortest run time
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestMaskRequestHostAPIParameter
Stack Traces | 4.6s run time
=== RUN   TestMaskRequestHostAPIParameter
=== PAUSE TestMaskRequestHostAPIParameter
=== CONT  TestMaskRequestHostAPIParameter
    mask_request_host_test.go:44: Command [python3] output: event:{start:{pid:1130}}
    mask_request_host_test.go:68: Command [cat] output: event:{start:{pid:1131}}
    mask_request_host_test.go:68: Command [cat] output: event:{data:{stderr:"cat: "}}
    mask_request_host_test.go:68: Command [cat] output: event:{data:{stderr:"/tmp/nc_output.txt: No such file or directory\n"}}
    mask_request_host_test.go:68: Command [cat] output: event:{end:{exit_code:1 exited:true status:"exit status 1" error:"exit status 1"}}
    mask_request_host_test.go:69: 
        	Error Trace:	.../tests/proxies/mask_request_host_test.go:69
        	Error:      	Received unexpected error:
        	            	command cat in sandbox ib97ss3cyfd16oshtd3uf failed with exit code 1
        	Test:       	TestMaskRequestHostAPIParameter
--- FAIL: TestMaskRequestHostAPIParameter (4.60s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/templates::TestTemplateBuildFuseConfiguration
Stack Traces | 300s run time
=== RUN   TestTemplateBuildFuseConfiguration
=== PAUSE TestTemplateBuildFuseConfiguration
=== CONT  TestTemplateBuildFuseConfiguration
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Building template h5jup8evg2c3sxga7t42/90923b9a-9c3e-401e-afe7-9c73dc02cdb2
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [base] FROM ubuntu:22.04 [8887ef68ca5d31616e190e0f439f6fb730b3b72ee55d883db645de00acbe98a0]
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Base Docker image size: 30 MB
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Creating file system and pulling Docker image
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Uncompressing layer sha256:d6834b4a794c03efa2c998853e64969fa8851b11b2ade63292268872a37759d0 30 MB
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Uncompressing layer sha256:363aa651b381ead5923bc5ef781848bdcb1bb0e8687d565f654e3cf3576c8209 13 MB
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Uncompressing layer sha256:e216621d8a0a7a4486d6a228759b29bc378eff95e6ea9469854b8624b267dd13 137 B
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Layers extracted
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Root filesystem structure: bin, boot, dev, etc, home, lib, lib32, lib64, libx32, media, mnt, opt, proc, root, run, sbin, srv, sys, tmp, usr, var
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Provisioning sandbox template
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Provisioning was successful, cleaning up
    build_template_test.go:133: test-ubuntu-fuse-config: [info] Sandbox template provisioned
    build_template_test.go:133: test-ubuntu-fuse-config: [info] CACHED [base] DEFAULT USER user [8123eb509cdd87536be00614d59d021ba65afaa4135fe94f6b13c6bdd5a9a345]
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [builder 1/2] RUN grep -q 'z /dev/fuse 0666 root root -' /etc/tmpfiles.d/fuse.conf [55f343c8982ec6ae114e5ec2ae0f3ca830a76a36365eec36a82d7526bcfaf58f]
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [builder 2/2] RUN echo "Checking /dev/fuse permissions:"; ls -la /dev/fuse; stat -c 'mode=%a owner=%U group=%G' /dev/fuse; test $(stat -c %a /dev/fuse) = '666' [d119dd9950ffb13645cda3d64c82eb3451c17afeeaed7cceb5a7d51967c43c90]
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [builder 2/2] [stdout]: Checking /dev/fuse permissions:
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [builder 2/2] [stdout]: crw-rw-rw- 1 root root 10, 229 Jul 27 09:26 /dev/fuse
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [builder 2/2] [stdout]: mode=666 owner=root group=root
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [finalize] Finalizing template build [22029e2c1bec516a052a860aec32bfac1e512e643d04ba5981c09065ebeafddd]
    build_template_test.go:133: test-ubuntu-fuse-config: [info] [optimize] Optimizing template [eb098a98fde3f86bcd6e3f785da289ff0384519b6292f9a978a37deeb68340ac]
    build_template_test.go:1189: 
        	Error Trace:	.../api/templates/build_template_test.go:96
        	            				.../api/templates/build_template_test.go:1189
        	Error:      	Received unexpected error:
        	            	Get "http://localhost:.../builds/90923b9a-9c3e-401e-afe7-9c73dc02cdb2/status?logsOffset=20&level=info": context deadline exceeded
        	Test:       	TestTemplateBuildFuseConfiguration
--- FAIL: TestTemplateBuildFuseConfiguration (300.27s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestCACertTrustedAfterFilesystemOnlyReboot
Stack Traces | 301s run time
=== RUN   TestCACertTrustedAfterFilesystemOnlyReboot
=== PAUSE TestCACertTrustedAfterFilesystemOnlyReboot
=== CONT  TestCACertTrustedAfterFilesystemOnlyReboot
    template.go:43: test-fs-only-ca-reboot: [info] Building template mwn2mj08ltu8wecelrbl/80dd3cf6-1b59-464b-b12a-6f82894816c3
Executing command findmnt in sandbox ih4uwsgldh3azdztxa8vl (user: root)
    template.go:43: test-fs-only-ca-reboot: [info] [base] FROM ubuntu:22.04 [8887ef68ca5d31616e190e0f439f6fb730b3b72ee55d883db645de00acbe98a0]
    template.go:43: test-fs-only-ca-reboot: [info] Base Docker image size: 30 MB
    template.go:43: test-fs-only-ca-reboot: [info] Creating file system and pulling Docker image
    template.go:43: test-fs-only-ca-reboot: [info] Uncompressing layer sha256:d6834b4a794c03efa2c998853e64969fa8851b11b2ade63292268872a37759d0 30 MB
    template.go:43: test-fs-only-ca-reboot: [info] Uncompressing layer sha256:363aa651b381ead5923bc5ef781848bdcb1bb0e8687d565f654e3cf3576c8209 13 MB
    template.go:43: test-fs-only-ca-reboot: [info] Uncompressing layer sha256:e216621d8a0a7a4486d6a228759b29bc378eff95e6ea9469854b8624b267dd13 137 B
Executing command python in sandbox it4vx9k52s8hm02egmh92
    template.go:43: test-fs-only-ca-reboot: [info] Layers extracted
    template.go:43: test-fs-only-ca-reboot: [info] Root filesystem structure: bin, boot, dev, etc, home, lib, lib32, lib64, libx32, media, mnt, opt, proc, root, run, sbin, srv, sys, tmp, usr, var
    template.go:43: test-fs-only-ca-reboot: [info] Provisioning sandbox template
    template.go:43: test-fs-only-ca-reboot: [info] Provisioning was successful, cleaning up
    template.go:43: test-fs-only-ca-reboot: [info] Sandbox template provisioned
    template.go:43: test-fs-only-ca-reboot: [info] CACHED [base] DEFAULT USER user [8123eb509cdd87536be00614d59d021ba65afaa4135fe94f6b13c6bdd5a9a345]
    template.go:43: test-fs-only-ca-reboot: [info] [builder 1/1] RUN echo 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlekNDQVNHZ0F3SUJBZ0lCQVRBS0JnZ3Foa2pPUFFRREFqQWxNU013SVFZRFZRUURFeHBsTW1JdGRHVnoKZEMxallTMW1jeTF2Ym14NUxYSmxZbTl2ZERBZUZ3MHlOakEzTWpjd09USXhORGxhRncweU5qQTNNamN4TURJeQpORGxhTUNVeEl6QWhCZ05WQkFNVEdtVXlZaTEwWlhOMExXTmhMV1p6TFc5dWJIa3RjbVZpYjI5ME1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUV2S1pCTnBRalB0bmw3Umc1b3UwdEdKVVBVN3NhYTJrb1B6UWIKRWxjR0VPczdBQ3E1UU9MQ0R0NDhOVkNwUjQwRHBEUEZvQmJPZFRrWkRudnQ5SmdFVTZOQ01FQXdEZ1lEVlIwUApBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGSkFabGVWb0F0SkV5K3ozCkVDRVZMM0NMaE9iVk1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lHR1pVd3dEaXFUUXFYUVRrWXBwZXRMSzNvSzUKSTUxWmFwQ05raVdkRVZDUUFpRUFrWEVYWmgrVUU2TExMY0daM0NNVmdDMThNdGVoajFRMXkzcG05eTVKNkJNPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' | base64 -d > .../share/ca-certificates/e2b-reboot-ca.crt root [56f33cde37c59c48ac17ae12bcfd066bf15b699fe66a5727628d0daad0809fe7]
    template.go:43: test-fs-only-ca-reboot: [info] [finalize] Finalizing template build [7e56fcd5ee5b2112eb83a19bdcca288f4e73ec692bf0a3db1702826ecfd30c70]
    template.go:43: test-fs-only-ca-reboot: [info] [optimize] Optimizing template [f66ae049a8babcbe9af0e8abaaee5e112f12897e6b1da803396b340a5b170ad1]
    ca_cert_reboot_test.go:30: Build timeout exceeded
--- FAIL: TestCACertTrustedAfterFilesystemOnlyReboot (300.83s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/templates::TestUpdateTemplateWithAPIKey
Stack Traces | 301s run time
=== RUN   TestUpdateTemplateWithAPIKey
=== PAUSE TestUpdateTemplateWithAPIKey
=== CONT  TestUpdateTemplateWithAPIKey
    template_update_test.go:148: Build timeout exceeded
--- FAIL: TestUpdateTemplateWithAPIKey (300.90s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Comment thread packages/orchestrator/pkg/template/build/phases/base/files.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68d6b0ad2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +83 to +96
Key: "arch",
IDs: []string{"arch", "archarm"},
Packages: []string{
"systemd", "shadow", "openssh", "sudo", "chrony", "socat", "curl",
"ca-certificates", "fuse3", "iptables", "git", "nfs-utils", "less",
"nftables", "iputils", "jq", "bash",
},
PkgQueryBody: `pacman -Q "$1" >/dev/null 2>&1`,
PkgInstall: "pacman -Sy --noconfirm\n pacman -S --noconfirm --needed \"$@\"",
InitBinary: "/usr/lib/systemd/systemd",
TimeSyncUnit: "chronyd",
AdminGroup: "wheel",
CABundle: "/etc/ssl/certs/ca-certificates.crt",
CARefresh: "update-ca-certificates",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The Arch profile's PkgInstall runs pacman -Sy --noconfirm (sync package databases) followed by a plain pacman -S --noconfirm --needed "$@" (no -u), which is the Arch-documented-unsupported partial-upgrade pattern.

Extended reasoning...

The bug: in distro.go's arch Profile, PkgInstall is:

PkgInstall: "pacman -Sy --noconfirm\n    pacman -S --noconfirm --needed \"$@\"",

pacman -Sy refreshes the local package databases to the latest state of the configured repos, but does not upgrade any already-installed packages. The subsequent pacman -S --needed "$@" then resolves and installs the new packages against that freshly-synced database. This is exactly the "partial upgrade" pattern that the Arch Wiki explicitly documents as unsupported: because Arch is a rolling release with no cross-version ABI/compatibility guarantees between package versions, a newly-installed package (or one of its direct dependencies) can end up linked against a newer shared-library/glibc version than what's already present on the rest of the (un-upgraded) base image. The correct, supported invocation is pacman -Syu --noconfirm (sync + full system upgrade) before/while installing new packages.

Trigger path: provision.sh's e2b_pkg_install is only invoked when MISSING is non-empty — i.e., when one or more of the profile's declared packages (systemd, openssh, sudo, chrony, socat, curl, ca-certificates, fuse3, git, nfs-utils, less, nftables, iputils, jq, bash) is absent from the base image. On a minimal Arch base (e.g. the official archlinux OCI image), most of these will be missing, so this code path runs on essentially every Arch template build, not as a rare edge case.

Why nothing prevents it: there's no -u flag, no separate pacman -Syu step, and no test asserting the invocation is upgrade-safe — distro_test.go's TestFamiliesDiffer only checks that the arch install string contains "pacman", it doesn't validate the flags used.

Impact: because Arch base images here are pulled from OCI/dockerhub, they can lag the live Arch repos by an arbitrary amount of time (dockerhub images aren't rebuilt on every repo update). The longer that lag, the more likely a partial upgrade pulls in a package built against newer shared libraries than what's on the stale base, which can manifest as broken dependency resolution during install, or as binaries that crash or misbehave at runtime after boot — a failure mode that's intermittent and hard to diagnose after the fact, since it depends on the staleness of whatever base image happened to be pulled.

Proof walkthrough:

  1. Arch OCI base image is built from a repo snapshot at time T0.
  2. Time passes; upstream Arch repos move to T1, where some library libfoo.so.2 has been superseded by libfoo.so.3 and a dependent package bar has been rebuilt against libfoo.so.3.
  3. Template build runs e2b_pkg_install, which does pacman -Sy — this syncs the local package DB metadata to T1, but the base image's installed packages (including whatever already depends on libfoo.so.2) remain at T0.
  4. pacman -S --needed bar (one of the MISSING packages, or pulled in as its dependency) installs bar at its T1 build, dynamically linked against libfoo.so.3.
  5. If libfoo.so.2 is still what's installed on the base (never upgraded because -u was never run), bar fails to resolve libfoo.so.3 at runtime and crashes, or dependency resolution during the pacman -S step itself fails/conflicts.

Fix: change the arch profile's PkgInstall to run pacman -Syu --noconfirm before/instead of the bare -Sy, e.g. "pacman -Syu --noconfirm\n pacman -S --noconfirm --needed \"$@\"", matching the Arch-documented supported invocation.

Severity note: this only affects the new Arch profile (v1, no existing templates to regress), and the failure is probabilistic — it only manifests when the pulled base image has actually lagged the live repos and a newly-installed package needs a newer shared library. It doesn't guarantee a build failure on every run. The fix itself is a one-token change with no meaningful downside. Given all four independent verifiers leaned toward "nit" (with one flagging it as defensibly either way), this is a worthwhile correctness fix but not a merge-blocker.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — the Arch profile uses pacman -Sy then -S (the partial-upgrade pattern Arch discourages). Routed to the engineer to move to -Syu (or document the fresh-base assumption) and verify on a real Arch build. Open until resolved.

Comment on lines +66 to +81
Key: "rhel",
// Fedora, RHEL, CentOS Stream, Rocky, Alma, Oracle Linux, Amazon Linux.
IDs: []string{"fedora", "rhel", "centos", "rocky", "almalinux", "ol", "amzn"},
Packages: []string{
"systemd", "shadow-utils", "passwd", "openssh-server", "sudo", "chrony",
"socat", "curl", "ca-certificates", "fuse3", "iptables-nft", "git",
"nfs-utils", "less", "nftables", "iputils", "jq", "bash",
},
PkgQueryBody: `rpm -q "$1" >/dev/null 2>&1`,
PkgInstall: `dnf -y --allowerasing install "$@" 2>/dev/null || microdnf -y install "$@"`,
InitBinary: "/usr/lib/systemd/systemd",
TimeSyncUnit: "chronyd",
AdminGroup: "wheel",
CABundle: "/etc/ssl/certs/ca-certificates.crt",
CARefresh: "update-ca-trust extract",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The rhel Profile's PkgInstall (dnf ... 2>/dev/null || microdnf ...) has no yum fallback, but the profile's own declared IDs include centos and amzn — CentOS 7 and Amazon Linux 2 ship yum only (no dnf/microdnf), so provisioning a declared-supported base with any missing package aborts the whole script under set -eu. The 2>/dev/null on the dnf branch also hides the real 'command not found' cause, leaving only microdnf's failure in the logs. A one-line || yum -y install "$@" fallback fixes it.

Extended reasoning...

The rhel Profile in distro.go registers IDs: []string{"fedora", "rhel", "centos", "rocky", "almalinized", "ol", "amzn"} and sets:

PkgInstall: `dnf -y --allowerasing install "$@" 2>/dev/null || microdnf -y install "$@"`,

There is no yum fallback. CentOS 7 (ID=centos) and Amazon Linux 2 (ID=amzn) are both explicitly claimed by this profile via their os-release ID, but neither ships dnf or microdnfdnf was introduced in RHEL/CentOS 8 and Amazon Linux 2023, and microdnf only appears in dnf-based minimal container images (e.g. ubi-minimal), not on centos:7 or amazonlinux:2.

Trigger path: provision.sh sources /etc/os-release, sets E2B_DISTRO_ID, and the generated selector (ShellSelector() in distro.go) matches centos/amzn into the rhel case arm, defining e2b_pkg_install() from the string above. If any package in the required set (systemd, openssh-server, chrony, socat, fuse3, nftables, etc. — a plausible gap on a minimal base image) is missing, provision.sh calls e2b_pkg_install $MISSING unconditionally (not guarded by if/||). On CentOS 7 / AL2 both dnf and microdnf resolve to "command not found" (exit 127), so e2b_pkg_install returns non-zero. Because provision.sh runs under set -eu and the call isn't wrapped in a conditional, the entire script aborts immediately — the template build fails.

Why nothing else catches this: TestRHELFamilyAliases in distro_test.go only asserts that "centos" and "amzn" are present in the rhel profile's IDs slice — it never exercises the actual install command against a real CentOS 7 or AL2 rootfs, so the gap is invisible to the test suite. The PR's own validation section says the rendered provisioning was exercised against "real Ubuntu 22.04 / Fedora 40 / Alpine rootfses" — CentOS 7 / AL2 specifically were not part of that manual check either.

Secondary issue: the 2>/dev/null on the dnf branch swallows dnf's stderr, so on these bases only microdnf's "command not found" surfaces in the build logs, not dnf's. This directly undercuts the PR's stated design goal of "exit[ing] fast with a clear, customer-visible error" instead of an opaque package-manager failure — the actual root cause (no dnf on this release) is masked, and the user only sees the microdnf error.

Step-by-step proof (CentOS 7 base image):

  1. Base image os-release has ID="centos", VERSION_ID="7".
  2. provision.sh sets E2B_DISTRO_ID=centos, selector matches the rhel case arm.
  3. MISSING ends up non-empty for at least one required package not preinstalled on a minimal CentOS 7 image (e.g. nftables, fuse3, or jq).
  4. e2b_pkg_install $MISSING expands to dnf -y --allowerasing install <pkgs> 2>/dev/null || microdnf -y install <pkgs>.
  5. dnf: command not found (exit 127, stderr discarded) → falls through to microdnf: command not found (exit 127) → function returns 127.
  6. set -eu triggers immediate script exit with a non-zero status.
  7. provisionSandbox (provision.go) reads E2B_PROVISIONING_EXIT:<nonzero> and the build fails, even though centos is one of the profile's explicitly declared, "supported" IDs.

Fix: append a yum fallback, e.g. dnf -y --allowerasing install "$@" 2>/dev/null || microdnf -y install "$@" 2>/dev/null || yum -y install "$@".

Severity note: this is a real gap but it fails safely (a clear build-time error, not silent corruption or a runtime break), it's a strict improvement over pre-PR behavior where every non-Debian base failed, and the mainstream/actively-targeted RHEL-family bases (Fedora, RHEL8+/CentOS Stream, Rocky, Alma, Oracle Linux, AL2023) all use dnf and are unaffected. CentOS 7 and Amazon Linux 2 are also both EOL or near-EOL as of the current date. Given the trivial fix and the fact that this is incomplete coverage of a brand-new capability rather than a regression, I'm marking this a nit rather than blocking.

Comment thread packages/orchestrator/pkg/template/build/phases/base/files.go
@tomassrnka
tomassrnka marked this pull request as draft July 24, 2026 09:49
@tomassrnka

Copy link
Copy Markdown
Member Author

Validation update — verified through provisioning; template-build boot blocked by the local nested-virt env (not this change)

Ran this branch on a real infra-local Lima dev slot (nested-KVM, Apple Silicon). Honest status:

Verified (observed):

  • The orchestrator builds with this change (real CGO_ENABLED=1 GOOS=linux GOARCH=arm64 build) and the stack starts.
  • packages/orchestrator/pkg/template/build/phases/base/distro unit tests pass.
  • An Ubuntu 22.04 template build ran the base + provisioning phase successfully through the real orchestrator ("Provisioning was successful"; the base layer then cached), i.e. the distro selector + profile-driven provisioning executed correctly on the baseline path.
  • Provisioning was also exercised against real Ubuntu/Fedora/Alpine rootfses (container pre-check): apt fails on Fedora on main (command not found), profile-driven path provisions Ubuntu/Fedora cleanly; Alpine confirms it needs the OpenRC path (rejected here, as designed).

Blocked (environment, not this PR): the build then fails at the incremental-snapshot memory-diff step:

/machine-config ... "track_dirty_pages":false
/snapshot/load (Uffd) → resume → /snapshot/create → GET /memory/dirty
ERROR error waiting for fc process {"error":"exit status 148"}
error building step 1: pause and upload: ... failed to get dirty memory: connection refused

Firecracker crashes (exit 148) servicing GET /memory/dirty under ARM nested virtualization in the Lima dev VM. This reproduces on the Ubuntu baseline, in FC/snapshot code this PR does not touch, so it is a local nested-virt limitation of the dev environment — it blocks any template build's diff step there, regardless of distro. A booted Fedora template therefore needs a real (non-nested) KVM host or a staging cluster to complete end-to-end; the change itself is verified up to that boundary.

(Separately filed the infra-local dev-slot tooling fix that was needed to bring the slot up at all: e2b-dev/infra-local#10.)

@ValentaTomas
ValentaTomas force-pushed the main branch 2 times, most recently from 5aad415 to d71980e Compare July 25, 2026 22:53
@cla-bot

cla-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: root.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@cla-bot cla-bot Bot removed the cla-signed label Jul 26, 2026
@blacksmith-sh

This comment has been minimized.

@cla-bot

cla-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: root.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

tomassrnka added a commit that referenced this pull request Jul 26, 2026
Keeps #3381 focused on multi-distro. The fixed-5008 flake fix lands via #3401 on
main; this branch rebases onto main to pick it up (CI's api shard goes green then).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tvi pushed a commit that referenced this pull request Jul 26, 2026
…fixed-5008 flake) (#3401)

`TestGetOrConnectNode_CacheMiss_DiscoversAndConnects` bound a **fixed**
port (`consts.OrchestratorAPIPort` = 5008) for its fake gRPC server and
flaked with `address already in use` — on shared CI runners (parallel
shards) and on dev boxes running a real orchestrator on 5008.

The helper now binds an **ephemeral** port and returns it; the mocked
Nomad service registration carries that port, so discovery still dials
the right listener.

**Pre-existing flake, independent of the multi-distro work** — split out
of #3381 to keep that PR focused (per operator). Verified: `go test
./internal/orchestrator/ -run TestGetOrConnectNode|TestConnectToNode`
green locally; gofmt clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
tomassrnka and others added 8 commits July 27, 2026 08:26
…a/RHEL/Arch)

Template builds are Debian/Ubuntu-only today: provision.sh hardcodes apt/dpkg,
Debian package names, the /lib/systemd/systemd init path and the chrony unit, so
a Fedora/RHEL/Arch base image fails opaquely during provisioning.

This makes provisioning distro-aware by keying on the base image's DECLARED
/etc/os-release ID (ADR-010) — not by probing which package manager exists
(the axis rejected in #2941). Each supported family is a declared Profile in a
new `phases/base/distro` package; provision.sh selects the profile in-guest by
$ID via a selector generated from that registry, and uses its package set,
package-manager functions, init path, chrony unit, admin group and CA handling.
An unsupported distro exits fast with a clear, customer-visible error.

- new `phases/base/distro`: Profile registry (debian/ubuntu, RHEL family incl.
  rocky/alma/centos/rhel/ol/amzn, arch) + generated shell selector + unit tests.
- provision.sh: os-release detect + selector; profile-driven package check/
  install, init-link, chrony enable, CA-bundle ensure. Debian package set/query/
  init path preserved (AC2).
- rootfs.go: drop the static Debian chrony.service symlink; provision.sh now
  enables the distro-correct unit (chrony vs chronyd).
- configure.sh: adduser -> useradd (portable); admin group sudo||wheel.

Scope: systemd family only (v1). Alpine (OpenRC/musl) is explicitly rejected and
tracked separately. Egress-proxy CA trust on RHEL (envd /etc/pki path) is a
follow-up. Refs FEAT-145 / IMPL-145.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…enable cannot prune it

The multi-user.target.wants/envd.service link was written with a relative
target that resolves inside the .wants directory and dangles. provision.sh's
new offline `systemctl enable $E2B_TIMESYNC_UNIT` prunes dangling .wants
symlinks, silently disabling envd autostart on Fedora (build died at
'wait for envd'). Absolute target survives the prune; Ubuntu unaffected.

Real-KVM diagnosis: IMPL-145 qa.md QA10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
update-ca-trust extract regenerates /etc/pki/ca-trust/extracted/* but never
creates /etc/ssl/certs/ca-certificates.crt, so on Fedora envd.service's
ExecStartPre found no bundle and fell back to update-ca-certificates — which
does not exist there — failing the unit forever (observed: sshd up, envd
port never opens, build dies at 'wait for envd' even with the autostart
symlink restored). Link the bundle to the extracted PEM in e2b_ca_refresh,
and make the unit's regenerate fallback tolerate distros without
update-ca-certificates. Arch profile gets the correct update-ca-trust too.

Real-KVM diagnosis: IMPL-145 qa.md QA10/QA11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he baked wants-symlink

On the RHEL family the base image has no systemd package, so provisioning's
dnf install triggers systemd's RPM scriptlet 'systemctl preset-all', whose
Fedora policy ends with 'disable *' — deleting envd's baked autostart symlink
even when non-dangling (verified by chroot experiment on the real provisioned
rootfs: preset-all removes it, enable recreates it). apt has no such pass,
which is why Ubuntu never hit this. Enabling envd after the package
transaction guarantees autostart on every family.

Real-KVM diagnosis: IMPL-145 qa.md QA11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-end)

Two remaining blockers for RHEL-family template builds, both observed live
on real KVM (SSH into the failing guest):

1. provision.sh removes /etc/machine-id, so the template's next boot is a
   systemd FIRST boot — PID1 then applies the distro preset policy to all
   units, and the RHEL family's policy ends with 'disable *', deleting
   envd's autostart symlink at boot no matter how provisioning created it
   (observed: unit 'disabled; preset: disabled' in the live guest). Ship
   /etc/systemd/system-preset/00-e2b.preset ('enable envd.service') so
   every preset-all — RPM scriptlet or first boot — enables envd instead.

2. The DEFAULT USER build step used Debian's 'adduser --disabled-password
   --gecos' (exit 2 on Fedora, where adduser is useradd) and
   'usermod -aG sudo' (no sudo group there). Ported to useradd + a
   sudo-then-wheel fallback, mirroring configure.sh (W1 T4).

With these plus the CA-bundle fix, the Fedora 40 template build completes
on real x86 KVM.

Real-KVM diagnosis: IMPL-145 qa.md QA11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…late support (W5)

Profiles now declare their init family (systemd | openrc); everything
init-specific that runs at provisioning time is one declared shell block per
init system (distro/init.go), rendered into the selector as e2b_init_setup().
provision.sh keeps a single init-agnostic body; the systemd block is moved
verbatim so the Debian render stays behaviorally identical (AC2).

Alpine profile (apk, busybox init -> OpenRC runlevels): the OpenRC setup
replaces the one-shot provisioning inittab with the real boot sequence, wires
the sysinit/boot runlevels a container image ships without, and enables envd
via the baked /etc/init.d/envd (supervise-daemon, mirrors envd.service incl.
the CA-store seeding; inert on systemd images). Layer executor's envd restart
is now init-agnostic. Images without /etc/os-release are rejected with a
message naming the real problem.

Verified on real x86 KVM: alpine:3.24 builds end-to-end and a booted sandbox
returns Alpine os-release with envd supervised (qa.md QA12).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bsent (W3)

The process wrapper hardcoded /usr/bin/ionice and /usr/bin/nice; on images
without util-linux/coreutils (Alpine base, minimal/UBI-class) every spawned
process died with exit 127 — observed on real KVM as the base-phase sync
command failing right after envd came up. Resolve the helpers via LookPath
and build the prefix from what exists; the oom_score_adj write (pure /proc)
stays unconditional. TT-4 covers present/absent/partial helper sets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude added 11 commits July 27, 2026 08:26
…-harden the provisioning inittab (AC4/AC7)

Provisioning output is only logged at debug, so customers saw rejections as a
bare 'exit status: 1'. Keep a rolling tail of the guest's provisioning lines
and attach it to the failure error — which is user-visible — so unsupported
images fail with the actual reason and the supported-distro list.

Run the provisioning pipeline entirely through the baked busybox: bare images
(pure-Nix, distroless) ship no /bin/sh or sed, which previously swallowed the
rejection message exactly where it matters most. Observed on real KVM with
nixos/nix: the build log now names the missing os-release and lists supported
distros.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Debian's systemctl enable synchronizes SysV state via update-rc.d, which
aborts on a non-LSB /etc/init.d/envd and failed the whole Ubuntu provisioning
(caught by the new failure-tail in the build error). Bake the script at
/usr/local/share/e2b/envd.openrc and install it to /etc/init.d only in the
OpenRC family's init setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ependency

chronyd treats a missing PHC refclock as FATAL, and /dev/ptp0 does not exist
where nested virtualization can't expose kvm-ptp (dev slots) — observed on
real KVM: chrony/chronyd dead in Ubuntu, Fedora AND Alpine guests (a
pre-existing baseline issue, not a FEAT-145 regression). Reference the PHC
only when the device exists at provisioning time (same host/KVM as runtime)
and fall back to an NTP pool otherwise. On Alpine, chronyd's 'need net' pulled
in the networking service, which errored on a missing /etc/network/interfaces;
a loopback-only interfaces file lets it provide 'net' without touching the
kernel-managed eth0. Production (kvm-ptp present) keeps the exact PHC config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cache key (W1 T5)

The fallback provision version hashed only the raw provision.sh template and
the embedded files/* — changes to the distro profiles/init blocks (rendered
into the script at build time) or to the baked symlink layer did not rotate
the key, silently reusing stale provisioned bases (bit dev twice, qa.md QA11).
Add distro.Fingerprint() (sha256 of the generated selector + an explicit
Version) to the fallback hash, and fold the symlink-layer map into FilesHash.
Production rollout stays on the BuildProvisionVersion flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(W5 T4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t; explicit failure paths everywhere

NixOS tier (qa.md QA13 proposal, operator-approved): a 'nixos' profile for
PREMADE images built from the E2B NixOS configuration — no package manager to
drive (everything is declared in the image), stage-2 init at the profile
symlink, and a Bootstrap hook that puts the baked busybox's applets on PATH
(no FHS userland exists before the first activation).

The provisioning boot itself is now self-contained on the baked busybox:
- rcS drives mkdir/mount through it (bare images have neither on PATH)
- busybox init hands any inittab line with shell metacharacters to /bin/sh,
  which bare images don't have — the pipeline moved into a baked runner
  script and every inittab entry is a plain exec; fsfreeze falls back to
  sync-only with a message where util-linux is absent

Failure-path policy (operator directive): no '|| true' silencing — explicit
checks with stated code paths. CA refresh failures now fail provisioning
loudly; missing OpenRC scripts / skel / passwd are checked and logged as
deliberate skips; user creation and admin-group failures are hard errors;
cert seeding is one shared warn-and-continue script (e2b-seed-certs) used by
both envd services instead of two silent fallback chains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n real KVM

The nixos-base-image/ directory is the versioned source of the premade image
(configuration.nix + build/publish script + operational notes). Boot fixes
found by observation on the real console/guest:

- NixOS activation refuses to symlink /etc/systemd/system over the baked
  drop-ins, leaving systemd with no units at all ('Unit default.target not
  found', frozen boot) — the nixos init setup removes them; the image's own
  configuration declares envd
- premade images lack /etc/profile.d, /root and /usr/sbin pre-activation —
  the shared provisioning body creates them
- the image declares /bin/bash (build commands invoke it explicitly), a
  'user' group matching useradd semantics (configure.sh chowns user:user),
  the exact sudoers line the build steps grep for, and the journald
  watchdog override

Verified end-to-end: template builds in 18s; booted sandbox returns
ID=nixos / NixOS 24.05, envd active, chronyd active, nix-env on PATH,
HTTPS CA trust works (qa.md QA14).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
coreutils >= 9.2 (Fedora 40, Arch) makes 'cp -n' exit 1 when it skips an
existing file — the previous '|| true' masked this real semantic landmine and
removing it broke Fedora finalize (observed: 'cp: not replacing ...bashrc',
exit 1). Walk /etc/skel explicitly and copy only missing files; real copy
failures stay loud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The envd-update step restarts envd via a command executed BY envd. systemctl
hands the restart to PID1 and survives envd's death; rc-service runs it
synchronously in the spawned shell, which envd's shutdown kills between the
stop and start halves — the new envd never starts and the update times out
(observed on Alpine finalize). setsid + background lets the restart outlive
its parent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c-service

The detached rc-service restart raced the next build command against the old
envd's death (observed: configure step hit 'unexpected EOF'). Killing envd
and letting supervise-daemon's respawn start the replaced binary makes the
old instance's death immediate — the post-update wait can only ever see the
new envd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…i port-collision flake

- lint: nlreturn/paralleltest/modernize findings in the FEAT-145 test files
  (blank lines before return/continue, t.Parallel() on all distro tests,
  range strings.SplitSeq)
- api: TestGetOrConnectNode_CacheMiss_DiscoversAndConnects bound a FIXED
  port (consts.OrchestratorAPIPort, 5008) and flaked with 'address already
  in use' wherever anything held it (persistent CI runners, dev boxes with a
  real orchestrator). The nomad discovery dials the registration's own
  address:port, so the fake server now uses an ephemeral port carried
  through the mocked registration — no fixed bind at all.

Gates run locally (matching .github/workflows): golangci-lint v2.11.4 → 0
issues on envd, orchestrator, api; go test ./... green on all seven shards
(api/client-proxy/db/docker-reverse-proxy/envd/orchestrator/shared);
GOARCH=arm64 build+vet green (CGO orchestrator via aarch64-linux-gnu-gcc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tomassrnka
tomassrnka force-pushed the feat/multi-distro-templates branch from e048b52 to e101748 Compare July 27, 2026 06:27
@cla-bot

cla-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: root.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@cla-bot

cla-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: root.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@e2b-dev e2b-dev deleted a comment from cla-bot Bot Jul 27, 2026
@tomassrnka

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 369027b. Configure here.

"nftables", "iputils", "jq", "bash",
},
PkgQueryBody: `pacman -Q "$1" >/dev/null 2>&1`,
PkgInstall: "pacman -Sy --noconfirm\n pacman -S --noconfirm --needed \"$@\"",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arch partial upgrade install

Medium Severity

Arch PkgInstall runs pacman -Sy then pacman -S, which refreshes the DB without upgrading installed packages. That partial-upgrade pattern can leave the guest with incompatible dependency versions during provisioning.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 369027b. Configure here.

@tomassrnka

Copy link
Copy Markdown
Member Author

Superseded by #3411 — reopened from a clean branch (internal refs stripped, NixOS split out to a stacked PR, review threads addressed at source). Closing this one.

@tomassrnka tomassrnka closed this Jul 27, 2026
tomassrnka added a commit to e2b-dev/E2B that referenced this pull request Jul 28, 2026
…ge helpers

Customer-facing delivery surface for multi-distro template support
(server side: e2b-dev/runtime#3381). Mirrors the existing Debian-family
convenience helpers (fromDebianImage/fromUbuntuImage/...) in both the
JS and Python SDKs:

- fromFedoraImage(variant='latest')  -> fedora:<variant>
- fromAlpineImage(variant='latest')  -> alpine:<variant>
- fromArchImage(variant='latest')    -> archlinux:<variant>
tomassrnka added a commit that referenced this pull request Jul 29, 2026
Resolve the base image's distro from its `/etc/os-release` ID and drive
provisioning from a declared per-family profile — Debian/Ubuntu (apt),
the RHEL family (dnf/microdnf/yum), Arch (pacman), and Alpine on OpenRC
(apk) — instead of probing for a package manager. Unsupported or
identity-less images are rejected with a clear build-log error.

Once merged, the biggest change is adduser -> useradd and introduction
of /usr/local/bin/e2b-seed-certs instead of long one-liner for envd.

Supersedes #3381 (reopened from a clean branch, no bot-thread churn).
NixOS support follows in a stacked PR #3412.
jakubno pushed a commit that referenced this pull request Aug 3, 2026
Resolve the base image's distro from its `/etc/os-release` ID and drive
provisioning from a declared per-family profile — Debian/Ubuntu (apt),
the RHEL family (dnf/microdnf/yum), Arch (pacman), and Alpine on OpenRC
(apk) — instead of probing for a package manager. Unsupported or
identity-less images are rejected with a clear build-log error.

Once merged, the biggest change is adduser -> useradd and introduction
of /usr/local/bin/e2b-seed-certs instead of long one-liner for envd.

Supersedes #3381 (reopened from a clean branch, no bot-thread churn).
NixOS support follows in a stacked PR #3412.
@ValentaTomas
ValentaTomas deleted the feat/multi-distro-templates branch September 11, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants