diff --git a/packages/orchestrator/pkg/template/build/phases/base/distro/distro.go b/packages/orchestrator/pkg/template/build/phases/base/distro/distro.go index cfda4874f8..45fb9f1190 100644 --- a/packages/orchestrator/pkg/template/build/phases/base/distro/distro.go +++ b/packages/orchestrator/pkg/template/build/phases/base/distro/distro.go @@ -1,8 +1,9 @@ // Package distro makes template-build provisioning distro-aware: it selects a // declared per-family Profile by the base image's /etc/os-release ID rather than // probing for a package manager. Supported: the systemd family (Debian/Ubuntu, -// Fedora/RHEL/CentOS/Rocky/Alma, Arch) and Alpine on OpenRC; anything else is -// rejected with a clear error. +// Fedora/CentOS/Rocky/Alma, Arch) and Alpine on OpenRC. Derivatives are +// provisioned via ID_LIKE as a best-effort guess with a warning; kernel-dependent +// ids (RejectedIDs) and anything unmatched are rejected with a clear error. package distro import ( @@ -178,13 +179,31 @@ func SupportedIDs() []string { return ids } +// RejectedIDs are distro ids we refuse even though their ID_LIKE names a +// family we do support. Oracle Linux and Amazon Linux both declare +// ID_LIKE=fedora, so without this the ID_LIKE fallback below would quietly +// re-admit exactly the images the rhel profile documents as out of scope. +var RejectedIDs = []string{"rhel", "ol", "amzn"} + // ShellSelector generates the POSIX-sh block provision.sh sources: it switches // on the guest's $E2B_DISTRO_ID and defines the profile's packages, shell -// functions, init path, time-sync unit, admin group and CA handling. An -// unrecognized id exits 1 with a customer-visible error. +// functions, init path, time-sync unit, admin group and CA handling. +// +// An unknown id retries each $E2B_ID_LIKE token in order and provisions the +// first matching family — best effort, with a customer-visible warning. +// RejectedIDs, ids matching nothing, and images without /etc/os-release exit 1. func ShellSelector() string { var b strings.Builder - b.WriteString(`case "$E2B_DISTRO_ID" in` + "\n") + + // Selection lives in a function so it can be retried per ID_LIKE token. + // Assignments and function definitions inside a POSIX-sh function are + // global, so the caller sees the profile the same way it always has. + // The match is reported via e2b_profile_matched, not the return status — + // a function called as an if-condition runs with errexit suppressed, + // which would swallow Bootstrap failures inside a matched arm. + b.WriteString("e2b_select_profile() {\n") + b.WriteString(" e2b_profile_matched=\n") + b.WriteString(` case "$1" in` + "\n") for _, p := range Profiles { fmt.Fprintf(&b, " %s)\n", strings.Join(p.IDs, "|")) if p.Bootstrap != "" { @@ -201,14 +220,42 @@ func ShellSelector() string { fmt.Fprintf(&b, " e2b_ca_refresh() { %s; }\n", p.CARefresh) fmt.Fprintf(&b, " E2B_INIT_SYSTEM=%q\n", p.Init) fmt.Fprintf(&b, " e2b_init_setup() {\n%s\n }\n", indentBlock(initSetup[p.Init], " ")) + fmt.Fprintf(&b, " e2b_profile_matched=1\n") fmt.Fprintf(&b, " ;;\n") } - fmt.Fprintf(&b, " *)\n") + fmt.Fprintf(&b, " *)\n ;;\n") + b.WriteString(" esac\n}\n\n") + + b.WriteString(`e2b_select_profile "$E2B_DISTRO_ID"` + "\n") + b.WriteString(`if [ -z "$e2b_profile_matched" ]; then` + "\n") + + // Deliberate rejections are checked before the fallback, so they keep + // failing fast with their own reason instead of being matched by ID_LIKE. + fmt.Fprintf(&b, " case \"$E2B_DISTRO_ID\" in\n") + fmt.Fprintf(&b, " %s)\n", strings.Join(RejectedIDs, "|")) + fmt.Fprintf(&b, " echo \"[provision] ERROR: base image distribution ID='$E2B_DISTRO_ID' is not supported.\" >&2\n") + fmt.Fprintf(&b, " echo \"[provision] Sandboxes boot E2B's kernel, so the kABI, signed modules and SELinux these images are chosen for are unavailable.\" >&2\n") + fmt.Fprintf(&b, " exit 1\n") + fmt.Fprintf(&b, " ;;\n") + fmt.Fprintf(&b, " esac\n") + + b.WriteString(" e2b_like_match=\n") + b.WriteString(` for e2b_like in $E2B_ID_LIKE; do` + "\n") + b.WriteString(` e2b_select_profile "$e2b_like"` + "\n") + b.WriteString(` if [ -n "$e2b_profile_matched" ]; then` + "\n") + b.WriteString(" e2b_like_match=$e2b_like\n") + b.WriteString(" break\n") + b.WriteString(" fi\n") + b.WriteString(" done\n") + + b.WriteString(` if [ -z "$e2b_like_match" ]; then` + "\n") fmt.Fprintf(&b, " echo \"[provision] ERROR: unsupported base image distribution: ID='${E2B_DISTRO_ID:-unknown}'.\" >&2\n") fmt.Fprintf(&b, " echo \"[provision] E2B template builds support: %s.\" >&2\n", strings.Join(SupportedIDs(), ", ")) fmt.Fprintf(&b, " exit 1\n") - fmt.Fprintf(&b, " ;;\n") - b.WriteString("esac\n") + b.WriteString(" fi\n") + + fmt.Fprintf(&b, " echo \"[provision] WARNING: base image distribution ID='$E2B_DISTRO_ID' is not officially supported; provisioning it as '$e2b_like_match' from ID_LIKE. This is best effort and untested.\" >&2\n") + b.WriteString("fi\n") return b.String() } diff --git a/packages/orchestrator/pkg/template/build/phases/base/distro/distro_test.go b/packages/orchestrator/pkg/template/build/phases/base/distro/distro_test.go index 7f7096cb71..0804d6b2ad 100644 --- a/packages/orchestrator/pkg/template/build/phases/base/distro/distro_test.go +++ b/packages/orchestrator/pkg/template/build/phases/base/distro/distro_test.go @@ -202,3 +202,47 @@ func TestKernelDependentIDsAreRejected(t *testing.T) { } } } + +// An id we don't know falls back to ID_LIKE with a warning instead of failing: +// switching provisioning to the declared id silently dropped every Debian +// derivative (Kali declares ID=kali ID_LIKE=debian) that used to work back when +// we probed for a package manager. +func TestUnknownIDFallsBackToIDLike(t *testing.T) { + t.Parallel() + sel := ShellSelector() + if !strings.Contains(sel, "e2b_select_profile") { + t.Error("selection must be a function so it can be retried per ID_LIKE token") + } + if !strings.Contains(sel, "for e2b_like in $E2B_ID_LIKE; do") { + t.Error("selector must retry each ID_LIKE token") + } + if !strings.Contains(sel, "WARNING") { + t.Error("an ID_LIKE match must warn, not pass silently") + } + // Nothing matched is still fatal — better than provisioning a guessed family. + if !strings.Contains(sel, "unsupported base image distribution") { + t.Error("an id matching neither ID nor ID_LIKE must still fail") + } + // An if-condition call runs the function body with errexit suppressed. + if strings.Contains(sel, "if e2b_select_profile") || strings.Contains(sel, "if ! e2b_select_profile") { + t.Error("e2b_select_profile must not be invoked as an if-condition (errexit suppression)") + } + if !strings.Contains(sel, "e2b_profile_matched=1") { + t.Error("a matched profile arm must set e2b_profile_matched") + } +} + +// ID_LIKE must not re-admit the ids the rhel profile documents as out of scope: +// Oracle and Amazon Linux both declare ID_LIKE=fedora. +func TestRejectedIDsAreNotReachableViaIDLike(t *testing.T) { + t.Parallel() + sel := ShellSelector() + guard := strings.Join(RejectedIDs, "|") + if !strings.Contains(sel, guard) { + t.Errorf("selector must guard rejected ids (%s) before the ID_LIKE fallback", guard) + } + // The guard has to come first, or ID_LIKE=fedora would match them. + if strings.Index(sel, guard) > strings.Index(sel, "E2B_ID_LIKE") { + t.Error("the rejected-id guard must precede the ID_LIKE fallback") + } +} diff --git a/packages/orchestrator/pkg/template/build/phases/base/provision.sh b/packages/orchestrator/pkg/template/build/phases/base/provision.sh index 0801b43552..b544983b91 100644 --- a/packages/orchestrator/pkg/template/build/phases/base/provision.sh +++ b/packages/orchestrator/pkg/template/build/phases/base/provision.sh @@ -22,8 +22,12 @@ echo "Detecting base image distribution" if [ -r /etc/os-release ]; then . /etc/os-release E2B_DISTRO_ID="${ID:-unknown}" + # Derivative-to-parent pointer, e.g. Kali declares ID=kali ID_LIKE=debian. + # Only consulted when the declared ID matches no profile. + E2B_ID_LIKE="${ID_LIKE:-}" else E2B_DISTRO_ID="unknown (image has no /etc/os-release)" + E2B_ID_LIKE="" fi {{ .DistroSelector }} diff --git a/tests/integration/internal/tests/api/templates/distro_build_test.go b/tests/integration/internal/tests/api/templates/distro_build_test.go index f6077443ce..158a6bf9d0 100644 --- a/tests/integration/internal/tests/api/templates/distro_build_test.go +++ b/tests/integration/internal/tests/api/templates/distro_build_test.go @@ -77,7 +77,8 @@ func TestTemplateBuildDistroFamilies(t *testing.T) { // An image from a distro E2B doesn't provision must be rejected while // provisioning, and the reason must reach the customer instead of a bare exit -// status. Oracle Linux is deliberately unsupported: sandboxes boot E2B's kernel. +// status. Oracle Linux declares ID_LIKE=fedora, so this also covers the +// rejection guard running before the ID_LIKE fallback. func TestTemplateBuildUnsupportedDistro(t *testing.T) { t.Parallel() @@ -89,5 +90,6 @@ func TestTemplateBuildUnsupportedDistro(t *testing.T) { outcome := runTemplateBuild(t, "test-distro-unsupported", buildConfig, defaultBuildLogHandler(t)) require.False(t, outcome.ready, "Build of an unsupported distro must fail") - assert.Contains(t, outcome.reason, "unsupported base image distribution") + assert.Contains(t, outcome.reason, "ID='ol' is not supported") + assert.Contains(t, outcome.reason, "Sandboxes boot E2B's kernel") }