fix: enforce runner retention before job create

This commit is contained in:
lyon
2026-06-19 22:35:55 +08:00
parent 56ecb493ef
commit 1be0e8e02f
5 changed files with 587 additions and 15 deletions
+22 -6
View File
@@ -11,6 +11,8 @@ import { staticWorkReadyCapabilitySummary } from "../common/work-ready.js";
import { resolveRunnerEnvImage } from "../common/env-image-ref.js";
import { ensureSessionPvc } from "./session-pvc.js";
import { gitTransportSummary } from "../common/git-transport.js";
import { enforceRunnerRetentionBeforeCreate } from "./runner-retention.js";
import type { RunnerRetentionOptions, RunnerRetentionSummary } from "./runner-retention.js";
const reusableCredentialEnvNames = new Set([
"AUTH_PASSWORD",
@@ -44,9 +46,12 @@ export interface RunnerJobDefaults {
envIdentity?: string;
artifactCatalogFile?: string;
serviceAccountName?: string;
jobNamePrefix?: string;
lane?: string;
runnerIdleTimeoutMs?: number;
kubectlCommand?: string;
unideskSshEndpointEnv?: JsonRecord;
retention?: RunnerRetentionOptions;
}
export interface CreateRunnerJobInput extends JsonRecord {
@@ -83,12 +88,14 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
const managerUrl = optionalString(options.input.managerUrl) ?? options.defaults.managerUrl;
const sourceCommit = optionalString(options.input.sourceCommit) ?? options.defaults.sourceCommit;
const serviceAccountName = optionalString(options.input.serviceAccountName) ?? options.defaults.serviceAccountName;
const jobNamePrefix = options.defaults.jobNamePrefix;
const lane = options.defaults.lane;
const idempotencyKey = optionalString(options.input.idempotencyKey);
const transientEnv = assembleToolContextTransientEnv(run.executionPolicy, transientEnvField(options.input.transientEnv), options.defaults);
const attemptId = optionalString(options.input.attemptId) ?? `attempt_${Date.now().toString(36)}`;
const runnerId = optionalString(options.input.runnerId);
const runnerIdleTimeoutMs = optionalPositiveInteger(options.input.runnerIdleTimeoutMs, "runnerIdleTimeoutMs") ?? options.defaults.runnerIdleTimeoutMs;
const transientEnvSecretName = transientEnv.length > 0 ? transientEnvSecretNameForRun(run.id, commandId, attemptId) : null;
const transientEnvSecretName = transientEnv.length > 0 ? transientEnvSecretNameForRun(run.id, commandId, attemptId, jobNamePrefix) : null;
const renderTransientEnv = transientEnvSecretName ? transientEnvWithSecretRefs(transientEnv, transientEnvSecretName) : transientEnv;
const normalizedPayload = {
commandId,
@@ -110,6 +117,10 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
}
if (isTerminalRunStatus(run.status)) throw new AgentRunError(run.failureKind ?? (run.status === "cancelled" ? "cancelled" : "schema-invalid"), `run ${run.id} is already terminal: ${run.status}`, { httpStatus: 409 });
if (isTerminalCommandState(command.state) || command.state !== "pending") throw new AgentRunError(command.state === "cancelled" ? "cancelled" : "schema-invalid", `command ${commandId} is not pending: ${command.state}`, { httpStatus: 409 });
let preCreateRetention: RunnerRetentionSummary | null = null;
if (options.defaults.retention) {
preCreateRetention = await enforceRunnerRetentionBeforeCreate({ store: options.store, options: { ...options.defaults.retention, namespace }, incomingRunnerCount: 1 });
}
let sessionPvc: RunnerSessionPvcOptions | undefined;
let sessionPvcSummary: JsonRecord | null = null;
if (run.sessionRef?.sessionId) {
@@ -156,6 +167,8 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
transientEnv: renderTransientEnv,
...(runnerIdleTimeoutMs !== undefined ? { runnerIdleTimeoutMs } : {}),
...(serviceAccountName ? { serviceAccountName } : {}),
...(jobNamePrefix ? { jobNamePrefix } : {}),
...(lane ? { lane } : {}),
...(sessionPvc ? { sessionPvc } : {}),
};
const render = renderRunnerJobManifest({ ...renderOptions, attemptId, ...(runnerId ? { runnerId } : {}) });
@@ -165,7 +178,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
let created: JsonRecord | null = null;
try {
if (transientEnvSecretName) {
await kubectlCreate(transientEnvSecretManifest({ namespace: render.namespace, name: transientEnvSecretName, runId: run.id, commandId, attemptId: render.attemptId, runnerId: render.runnerId, jobName: render.jobName, items: transientEnv }), kubectlCommand, "runner transient env secret");
await kubectlCreate(transientEnvSecretManifest({ namespace: render.namespace, name: transientEnvSecretName, runId: run.id, commandId, attemptId: render.attemptId, runnerId: render.runnerId, jobName: render.jobName, lane: lane ?? "v0.1", items: transientEnv }), kubectlCommand, "runner transient env secret");
transientEnvSecretCreated = true;
}
created = await kubectlCreate(render.manifest, kubectlCommand, "runner job");
@@ -220,6 +233,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
retention: {
ttlSecondsAfterFinished: render.ttlSecondsAfterFinished,
runnerIdleTimeoutMs: render.runnerIdleTimeoutMs,
preCreateCleanup: preCreateRetention,
},
pollActions: [
runnerJobActionDescriptor({ action: "inspect-run", operation: "describe", resourceKind: "run", resourceName: run.id, runId: run.id }),
@@ -263,6 +277,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
envImage,
gitTransport: gitTransportSummary(),
workReady: staticWorkReadyCapabilitySummary(),
retention: preCreateRetention,
toolCredentials: summarizeToolCredentials(render.toolCredentials, render.namespace),
sessionPvc: sessionPvcSummary,
sessionRef: summarizeSessionRef(run.sessionRef ?? null),
@@ -357,11 +372,12 @@ function transientEnvWithSecretRefs(items: RunnerTransientEnv[], secretName: str
return items.map((item) => ({ ...item, secretRef: { name: secretName, key: item.name } }));
}
function transientEnvSecretNameForRun(runId: string, commandId: string, attemptId: string): string {
return `agentrun-v01-runner-env-${stableHash({ runId, commandId, attemptId }).slice(0, 20)}`;
function transientEnvSecretNameForRun(runId: string, commandId: string, attemptId: string, jobNamePrefix: string | undefined): string {
const prefix = (jobNamePrefix ?? "agentrun-v01-runner").toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "") || "agentrun-runner";
return `${prefix.slice(0, 36).replace(/-+$/u, "")}-env-${stableHash({ runId, commandId, attemptId }).slice(0, 20)}`;
}
function transientEnvSecretManifest(options: { namespace: string; name: string; runId: string; commandId: string; attemptId: string; runnerId: string; jobName: string; items: RunnerTransientEnv[] }): JsonRecord {
function transientEnvSecretManifest(options: { namespace: string; name: string; runId: string; commandId: string; attemptId: string; runnerId: string; jobName: string; lane: string; items: RunnerTransientEnv[] }): JsonRecord {
const stringData: JsonRecord = {};
for (const item of options.items) stringData[item.name] = item.value;
return {
@@ -374,7 +390,7 @@ function transientEnvSecretManifest(options: { namespace: string; name: string;
"app.kubernetes.io/name": "agentrun-runner",
"app.kubernetes.io/component": "runner-transient-env",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": "v0.1",
"agentrun.pikastech.local/lane": options.lane,
"agentrun.pikastech.local/run-hash": stableHash(options.runId).slice(0, 12),
"agentrun.pikastech.local/runner-job": options.jobName,
},