fix: enforce runner retention before job create
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user