diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index bb03dbd51..fc18b7f09 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -210,7 +210,14 @@ export const Env = z KUBERNETES_MEMORY_OVERHEAD_GB: z.coerce.number().min(0).optional(), // Optional memory overhead to add to the limit in GB KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods - KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z.string().default("profiles/block-io-uring.json"), + KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z + .string() + .trim() + .min(1) + .default("profiles/block-io-uring.json"), + KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z + .enum(["none", "node-24-plus", "all"]) + .default("node-24-plus"), // Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index a2e95ab79..542841bd6 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -653,6 +653,7 @@ class ManagedSupervisor { projectId: message.project.id, deploymentFriendlyId: message.deployment.friendlyId, deploymentVersion: message.backgroundWorker.version, + runtime: message.backgroundWorker.runtime, deploymentToken, runId: message.run.id, runFriendlyId: message.run.friendlyId, diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index b012f0321..e3023bc5d 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -100,24 +100,56 @@ describe("withNodeSelector", () => { }); describe("withRunnerSeccompProfile", () => { - it("applies the profile for every runtime, preserving pod security defaults", () => { - const podSpec = withRunnerSeccompProfile(basePodSpec, "profiles/example.json"); + const base = { + profilePath: "profiles/example.json", + runtimes: "node-24-plus" as const, + runtime: "node-24", + checkpointsEnabled: true, + }; - expect(podSpec).toMatchObject({ - ...basePodSpec, - securityContext: { - ...basePodSpec.securityContext, - seccompProfile: { - type: "Localhost", - localhostProfile: "profiles/example.json", - }, - }, - }); + const withProfile = { + ...basePodSpec, + securityContext: { + ...basePodSpec.securityContext, + seccompProfile: { type: "Localhost", localhostProfile: "profiles/example.json" }, + }, + }; + + it("applies the profile to node-24 and above under the default scope", () => { + for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toMatchObject( + withProfile + ); + } }); - it("leaves the pod spec untouched when no profile is configured", () => { - for (const profilePath of [undefined, ""]) { - expect(withRunnerSeccompProfile(basePodSpec, profilePath)).toBe(basePodSpec); + it("skips older runtimes under the default scope", () => { + for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toBe(basePodSpec); + } + }); + + it("applies the profile to every runtime under the all scope", () => { + for (const runtime of ["node", "node-22", "bun", "node-24", undefined]) { + expect( + withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "all", runtime }) + ).toMatchObject(withProfile); + } + }); + + it("applies nothing under the none scope, whatever the runtime", () => { + for (const runtime of ["node-24", "bun", "node-22"]) { + expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "none", runtime })).toBe( + basePodSpec + ); + } + }); + + it("applies nothing when checkpoints are disabled", () => { + for (const runtimes of ["none", "node-24-plus", "all"] as const) { + expect( + withRunnerSeccompProfile(basePodSpec, { ...base, runtimes, checkpointsEnabled: false }) + ).toBe(basePodSpec); } }); }); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 43b65f6bd..d09c86cf9 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -135,9 +135,12 @@ export class KubernetesWorkloadManager implements WorkloadManager { ); } } - const podSpec = this.opts.checkpointsEnabled - ? withRunnerSeccompProfile(basePodSpec, env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH) - : basePodSpec; + const podSpec = withRunnerSeccompProfile(basePodSpec, { + profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH, + runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES, + runtime: opts.runtime, + checkpointsEnabled: this.opts.checkpointsEnabled, + }); await this.k8s.core.createNamespacedPod({ namespace: this.namespace, diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index 8ed7edad8..6c6ddfc09 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -54,26 +54,44 @@ export function withNodeSelector( }; } +export type RunnerSeccompProfileOptions = { + profilePath: string; + runtimes: "none" | "node-24-plus" | "all"; + runtime: string | null | undefined; + checkpointsEnabled: boolean | undefined; +}; + /** - * Applies the runner seccomp profile. The profile is a node-local file installed - * outside this repo, so an empty path leaves the pod on the runtime default - - * pointing at a profile the nodes don't have fails pod creation. + * Applies the runner seccomp profile, which is a node-local file installed outside + * this repo - pointing a pod at a profile its node doesn't have fails pod creation, + * so every condition for skipping it lives here. + * + * "node-24-plus" matches the original rollout: node >= 24 always creates io_uring + * fds, which can't be checkpointed, and blocking io_uring_setup makes libuv fall + * back to epoll. Tolerates an "experimental-" prefix. "bun" matches only under "all". */ export function withRunnerSeccompProfile( podSpec: Omit, - profilePath: string | undefined + options: RunnerSeccompProfileOptions ): Omit { - if (!profilePath) { + if (!options.checkpointsEnabled || options.runtimes === "none") { return podSpec; } + if (options.runtimes === "node-24-plus") { + const match = options.runtime ? /^(?:experimental-)?node-(\d+)$/.exec(options.runtime) : null; + if (!match || Number(match[1]) < 24) { + return podSpec; + } + } + return { ...podSpec, securityContext: { ...podSpec.securityContext, seccompProfile: { type: "Localhost", - localhostProfile: profilePath, + localhostProfile: options.profilePath, }, }, }; diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index d0ca48d7a..eda9162c7 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -42,6 +42,8 @@ export interface WorkloadManagerCreateOptions { projectId: string; deploymentFriendlyId: string; deploymentVersion: string; + // Canonical runtime identifier (e.g. "node", "node-22", "node-24") + runtime?: string; // When set, overrides the TRIGGER_DEPLOYMENT_ID value the runner forwards as its identity header. deploymentToken?: string; runId: string;