feat(v0.1): runner Job 直接挂载 per-session PVC + env 透传
PR C 起步:k8s-job.ts 加 sessionPvc volume + env passthrough - src/runner/k8s-job.ts: 新 RunnerSessionPvcOptions 接口;manifest 多渲染 agentrun-sessions volume + volumeMount;env 多透传 AGENTRUN_SESSION_PVC_NAME / _NAMESPACE / _MOUNT_PATH / AGENTRUN_CODEX_ROLLOUT_SUBDIR - src/mgr/kubernetes-runner-job.ts: run 引用 session 时查 session storage kind=pvc 自动构造 sessionPvc 透传给 manifest 渲染;kind=evicted 已在 PR B 短路返回 session-store-evicted - selftest: 1 新 case runner-k8s-job-session-pvc-volume-and-env 验证 PVC volume + env 全套透传 后续 PR C 剩余:src/backend/codex-stdio.ts emit codex-rollout-storage-mounted 事件 + session-store-evicted 升级;3 个 codex-stdio 端到端 case。
This commit is contained in:
@@ -6,7 +6,7 @@ import type { AgentRunStore } from "./store.js";
|
|||||||
import type { JsonRecord } from "../common/types.js";
|
import type { JsonRecord } from "../common/types.js";
|
||||||
import { stableHash, validateEnvName } from "../common/validation.js";
|
import { stableHash, validateEnvName } from "../common/validation.js";
|
||||||
import { renderRunnerJobManifest } from "../runner/k8s-job.js";
|
import { renderRunnerJobManifest } from "../runner/k8s-job.js";
|
||||||
import type { RunnerTransientEnv } from "../runner/k8s-job.js";
|
import type { RunnerSessionPvcOptions, RunnerTransientEnv } from "../runner/k8s-job.js";
|
||||||
|
|
||||||
const reusableCredentialEnvNames = new Set([
|
const reusableCredentialEnvNames = new Set([
|
||||||
"AUTH_PASSWORD",
|
"AUTH_PASSWORD",
|
||||||
@@ -75,14 +75,18 @@ 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 (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 });
|
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 sessionPvc: RunnerSessionPvcOptions | undefined;
|
||||||
if (run.sessionRef?.sessionId) {
|
if (run.sessionRef?.sessionId) {
|
||||||
const session = await options.store.getSession(run.sessionRef.sessionId);
|
const session = await options.store.getSession(run.sessionRef.sessionId);
|
||||||
if (session?.storageKind === "evicted") {
|
if (session?.storageKind === "evicted") {
|
||||||
throw new AgentRunError("session-store-evicted", `session ${session.sessionId} storage has been evicted; create a new sessionId`, { httpStatus: 409, details: { sessionId: session.sessionId, pvcName: session.storagePvcName ?? null, pvcPhase: session.storagePvcPhase ?? null, valuesPrinted: false } });
|
throw new AgentRunError("session-store-evicted", `session ${session.sessionId} storage has been evicted; create a new sessionId`, { httpStatus: 409, details: { sessionId: session.sessionId, pvcName: session.storagePvcName ?? null, pvcPhase: session.storagePvcPhase ?? null, valuesPrinted: false } });
|
||||||
}
|
}
|
||||||
|
if (session?.storageKind === "pvc" && session.storagePvcName) {
|
||||||
|
const subdir = session.codexRolloutSubdir ?? "sessions";
|
||||||
|
sessionPvc = { pvcName: session.storagePvcName, namespace: session.storageNamespace ?? "agentrun-v01", mountPath: `/home/agentrun/.codex-${run.backendProfile}/${subdir}`, codexRolloutSubdir: subdir };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const renderOptions: Parameters<typeof renderRunnerJobManifest>[0] = {
|
||||||
const renderOptions = {
|
|
||||||
run,
|
run,
|
||||||
commandId,
|
commandId,
|
||||||
managerUrl,
|
managerUrl,
|
||||||
@@ -91,6 +95,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
|
|||||||
sourceCommit,
|
sourceCommit,
|
||||||
transientEnv,
|
transientEnv,
|
||||||
...(serviceAccountName ? { serviceAccountName } : {}),
|
...(serviceAccountName ? { serviceAccountName } : {}),
|
||||||
|
...(sessionPvc ? { sessionPvc } : {}),
|
||||||
};
|
};
|
||||||
const attemptId = optionalString(options.input.attemptId);
|
const attemptId = optionalString(options.input.attemptId);
|
||||||
const runnerId = optionalString(options.input.runnerId);
|
const runnerId = optionalString(options.input.runnerId);
|
||||||
|
|||||||
+23
-3
@@ -19,9 +19,17 @@ export interface RunnerJobRenderOptions {
|
|||||||
backoffLimit?: number;
|
backoffLimit?: number;
|
||||||
ttlSecondsAfterFinished?: number;
|
ttlSecondsAfterFinished?: number;
|
||||||
transientEnv?: RunnerTransientEnv[];
|
transientEnv?: RunnerTransientEnv[];
|
||||||
|
sessionPvc?: RunnerSessionPvcOptions;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunnerSessionPvcOptions {
|
||||||
|
pvcName: string;
|
||||||
|
namespace: string;
|
||||||
|
mountPath: string;
|
||||||
|
codexRolloutSubdir: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RunnerTransientEnv {
|
export interface RunnerTransientEnv {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -91,9 +99,10 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani
|
|||||||
const jobName = `agentrun-v01-runner-${shortDnsHash(options.run.id, attemptId)}`;
|
const jobName = `agentrun-v01-runner-${shortDnsHash(options.run.id, attemptId)}`;
|
||||||
const secretRefs = credentialProjections(options.run, namespace);
|
const secretRefs = credentialProjections(options.run, namespace);
|
||||||
const toolCredentials = toolCredentialProjections(options.run, namespace);
|
const toolCredentials = toolCredentialProjections(options.run, namespace);
|
||||||
|
const sessionPvc = options.sessionPvc;
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
if (secretRefs.length === 0) warnings.push("run executionPolicy.secretScope 未声明 provider SecretRef;runner 将按 secret-unavailable 上报,而不会降级直连外部凭据");
|
if (secretRefs.length === 0) warnings.push("run executionPolicy.secretScope 未声明 provider SecretRef;runner 将按 secret-unavailable 上报,而不会降级直连外部凭据");
|
||||||
const env = runnerEnv(options, { namespace, jobName, runnerId, attemptId, sourceCommit, secretRefs, toolCredentials });
|
const env = runnerEnv(options, { namespace, jobName, runnerId, attemptId, sourceCommit, secretRefs, toolCredentials, sessionPvc });
|
||||||
const manifest: JsonRecord = {
|
const manifest: JsonRecord = {
|
||||||
apiVersion: "batch/v1",
|
apiVersion: "batch/v1",
|
||||||
kind: "Job",
|
kind: "Job",
|
||||||
@@ -132,6 +141,7 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani
|
|||||||
volumeMounts: [
|
volumeMounts: [
|
||||||
{ name: "runner-home", mountPath: "/home/agentrun" },
|
{ name: "runner-home", mountPath: "/home/agentrun" },
|
||||||
...secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.projectionMountPath, readOnly: true })),
|
...secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.projectionMountPath, readOnly: true })),
|
||||||
|
...(sessionPvc ? [{ name: "agentrun-sessions", mountPath: sessionPvc.mountPath, readOnly: false }] : []),
|
||||||
],
|
],
|
||||||
resources: {
|
resources: {
|
||||||
requests: { cpu: "250m", memory: "512Mi" },
|
requests: { cpu: "250m", memory: "512Mi" },
|
||||||
@@ -144,7 +154,11 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
volumes: [{ name: "runner-home", emptyDir: {} }, ...secretRefs.map(secretVolume)],
|
volumes: [
|
||||||
|
{ name: "runner-home", emptyDir: {} },
|
||||||
|
...secretRefs.map(secretVolume),
|
||||||
|
...(sessionPvc ? [{ name: "agentrun-sessions", persistentVolumeClaim: { claimName: sessionPvc.pvcName } }] : []),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -152,7 +166,7 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani
|
|||||||
return { manifest, namespace, jobName, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, toolCredentials, warnings, ttlSecondsAfterFinished };
|
return { manifest, namespace, jobName, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, toolCredentials, warnings, ttlSecondsAfterFinished };
|
||||||
}
|
}
|
||||||
|
|
||||||
function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[]; toolCredentials: ToolCredentialProjection[] }): JsonRecord[] {
|
function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[]; toolCredentials: ToolCredentialProjection[]; sessionPvc: RunnerSessionPvcOptions | undefined }): JsonRecord[] {
|
||||||
const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile);
|
const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile);
|
||||||
const codexHome = selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(options.run.backendProfile);
|
const codexHome = selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(options.run.backendProfile);
|
||||||
return [
|
return [
|
||||||
@@ -180,6 +194,12 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string
|
|||||||
{ name: "HOME", value: "/home/agentrun" },
|
{ name: "HOME", value: "/home/agentrun" },
|
||||||
{ name: "CODEX_HOME", value: codexHome },
|
{ name: "CODEX_HOME", value: codexHome },
|
||||||
...(selectedSecret ? [{ name: "AGENTRUN_CODEX_SECRET_HOME", value: selectedSecret.projectionMountPath }] : []),
|
...(selectedSecret ? [{ name: "AGENTRUN_CODEX_SECRET_HOME", value: selectedSecret.projectionMountPath }] : []),
|
||||||
|
...(context.sessionPvc ? [
|
||||||
|
{ name: "AGENTRUN_SESSION_PVC_NAME", value: context.sessionPvc.pvcName },
|
||||||
|
{ name: "AGENTRUN_SESSION_PVC_NAMESPACE", value: context.sessionPvc.namespace },
|
||||||
|
{ name: "AGENTRUN_SESSION_PVC_MOUNT_PATH", value: context.sessionPvc.mountPath },
|
||||||
|
{ name: "AGENTRUN_CODEX_ROLLOUT_SUBDIR", value: context.sessionPvc.codexRolloutSubdir },
|
||||||
|
] : []),
|
||||||
...toolCredentialEnvVars(context.toolCredentials),
|
...toolCredentialEnvVars(context.toolCredentials),
|
||||||
...transientEnvVars(options.transientEnv ?? []),
|
...transientEnvVars(options.transientEnv ?? []),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -153,7 +153,49 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
} finally {
|
} finally {
|
||||||
await new Promise<void>((resolve) => serverWithKubectl.server.close(() => resolve()));
|
await new Promise<void>((resolve) => serverWithKubectl.server.close(() => resolve()));
|
||||||
}
|
}
|
||||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied"] };
|
const sessionRunRecord: RunRecord = {
|
||||||
|
id: "run-selftest-session-pvc",
|
||||||
|
tenantId: "unidesk",
|
||||||
|
projectId: "pikasTech/unidesk",
|
||||||
|
workspaceRef: { kind: "host-path", path: context.workspace },
|
||||||
|
sessionRef: { sessionId: "sess-selftest-runner-001" },
|
||||||
|
resourceBundleRef: null,
|
||||||
|
providerId: "G14",
|
||||||
|
backendProfile: "codex",
|
||||||
|
executionPolicy: { sandbox: "workspace-write", approval: "never", timeoutMs: 15_000, network: "default", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile: "codex", secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"] } }] } },
|
||||||
|
traceSink: null,
|
||||||
|
status: "pending",
|
||||||
|
terminalStatus: null,
|
||||||
|
failureKind: null,
|
||||||
|
failureMessage: null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
claimedBy: null,
|
||||||
|
leaseExpiresAt: null,
|
||||||
|
};
|
||||||
|
const sessionPvcRendered = renderRunnerJobDryRun({
|
||||||
|
run: sessionRunRecord,
|
||||||
|
commandId: "cmd-selftest-session-pvc",
|
||||||
|
managerUrl: server.baseUrl,
|
||||||
|
image: "127.0.0.1:5000/agentrun/agentrun-mgr-env:self-test",
|
||||||
|
sessionPvc: { pvcName: "agentrun-v01-session-sess-selftest-runner-001", namespace: "agentrun-v01", mountPath: "/home/agentrun/.codex-codex/sessions", codexRolloutSubdir: "sessions" },
|
||||||
|
});
|
||||||
|
const sessionPvcManifest = sessionPvcRendered.manifest as JsonRecord;
|
||||||
|
const sessionPvcSpec = sessionPvcManifest.spec as JsonRecord;
|
||||||
|
const sessionPvcTemplate = sessionPvcSpec.template as JsonRecord;
|
||||||
|
const sessionPvcPodSpec = sessionPvcTemplate.spec as JsonRecord;
|
||||||
|
const sessionPvcContainers = Array.isArray(sessionPvcPodSpec.containers) ? sessionPvcPodSpec.containers as JsonRecord[] : [];
|
||||||
|
const sessionPvcMounts = Array.isArray(sessionPvcContainers[0]?.volumeMounts) ? sessionPvcContainers[0].volumeMounts as JsonRecord[] : [];
|
||||||
|
const sessionPvcVols = Array.isArray(sessionPvcPodSpec.volumes) ? sessionPvcPodSpec.volumes as JsonRecord[] : [];
|
||||||
|
assert.ok(sessionPvcMounts.some((m) => m.name === "agentrun-sessions" && m.mountPath === "/home/agentrun/.codex-codex/sessions" && m.readOnly === false), "session pvc volume mount must be present");
|
||||||
|
assert.ok(sessionPvcVols.some((v) => typeof v === "object" && v !== null && (v as JsonRecord).persistentVolumeClaim !== undefined && ((v as JsonRecord).persistentVolumeClaim as JsonRecord).claimName === "agentrun-v01-session-sess-selftest-runner-001"), "session pvc volume must reference the per-session PVC");
|
||||||
|
const sessionPvcEnv = Array.isArray(sessionPvcContainers[0]?.env) ? sessionPvcContainers[0].env as JsonRecord[] : [];
|
||||||
|
const envMap = new Map(sessionPvcEnv.map((e) => [String(e.name), String(e.value)]));
|
||||||
|
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAME"), "agentrun-v01-session-sess-selftest-runner-001");
|
||||||
|
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
|
||||||
|
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
|
||||||
|
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
|
||||||
|
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||||
} finally {
|
} finally {
|
||||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user