feat(supervisor): gate the runner seccomp profile by runtime scope
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<k8s.V1PodSpec, "containers">,
|
||||
profilePath: string | undefined
|
||||
options: RunnerSeccompProfileOptions
|
||||
): Omit<k8s.V1PodSpec, "containers"> {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user