feat: dedicated PVC per agent for OPENCODE_DB (FAR-63, Option B)
Replaces the Option A shared-PVC path implementation with a long-lived dedicated PVC per agent, mounted at /opencode-db with OPENCODE_DB=/opencode-db. Changes: - k8s-client.ts: add getPvc/createPvc/deletePvc CoreV1Api helpers - execute.ts: add ensureAgentDbPvc() that gets-or-creates a PVC named opencode-db-<agentId> before Job creation; pass agentDbClaimName through to buildJobManifest; return null for ephemeral mode (emptyDir used instead) - job-manifest.ts: accept agentDbClaimName on JobBuildInput; mount dedicated PVC or emptyDir at /opencode-db; set OPENCODE_DB=/opencode-db; revert init container to simple form (no mkdir, no PVC mount) - config-schema.ts: replace opencodeDbMode/opencodeDbPath with agentDbMode (dedicated_pvc|ephemeral, default dedicated_pvc), agentDbStorageClass (required for dedicated_pvc), agentDbStorageCapacity (default 1Gi) - test.ts: add create/delete RBAC checks for persistentvolumeclaims - pvc.test.ts: unit tests for ensureAgentDbPvc (7 cases incl. error paths) - 289/289 tests pass; typecheck clean - No agent-delete hook exists; opencode-db PVC janitor routine is a deferred follow-up task Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+34
-38
@@ -30,6 +30,12 @@ export interface JobBuildInput {
|
||||
* Required when the prompt exceeds LARGE_PROMPT_THRESHOLD_BYTES.
|
||||
*/
|
||||
promptSecretName?: string;
|
||||
/**
|
||||
* Claim name of the dedicated agent DB PVC (dedicated_pvc mode) or null for ephemeral emptyDir.
|
||||
* When provided (string or null), mounts /opencode-db and sets OPENCODE_DB=/opencode-db.
|
||||
* When undefined, no opencode-db volume is added.
|
||||
*/
|
||||
agentDbClaimName?: string | null;
|
||||
}
|
||||
|
||||
export interface JobBuildResult {
|
||||
@@ -233,13 +239,6 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
const configuredCwd = asString(config.cwd, "");
|
||||
const workingDir = workspaceCwd || configuredCwd || "/paperclip";
|
||||
|
||||
// OpenCode DB configuration
|
||||
const opencodeDbMode = (asString(config.opencodeDbMode, "shared_pvc").trim() || "shared_pvc") as "shared_pvc" | "ephemeral";
|
||||
const opencodeDbPathOverride = asString(config.opencodeDbPath, "").trim();
|
||||
const resolvedDbPath = opencodeDbMode === "ephemeral"
|
||||
? "/opencode-db"
|
||||
: (opencodeDbPathOverride || `/paperclip/.opencode/db/${agent.id}`);
|
||||
|
||||
// Job naming: slug + 6-char hash for collision resistance; strip trailing hyphens
|
||||
const agentSlug = sanitizeForK8sName(agent.id);
|
||||
const runSlug = sanitizeForK8sName(runId);
|
||||
@@ -303,12 +302,14 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
// Build env vars
|
||||
const envVars = buildEnvVars(ctx, selfPod, config);
|
||||
|
||||
// OPENCODE_DB: always set after user overrides to enforce per-agent isolation or ephemeral mode
|
||||
const dbEnvIdx = envVars.findIndex((e) => e.name === "OPENCODE_DB");
|
||||
if (dbEnvIdx >= 0) {
|
||||
envVars[dbEnvIdx] = { name: "OPENCODE_DB", value: resolvedDbPath };
|
||||
} else {
|
||||
envVars.push({ name: "OPENCODE_DB", value: resolvedDbPath });
|
||||
// OPENCODE_DB: set when a DB volume is present (dedicated PVC or ephemeral emptyDir)
|
||||
if (input.agentDbClaimName !== undefined) {
|
||||
const dbEnvIdx = envVars.findIndex((e) => e.name === "OPENCODE_DB");
|
||||
if (dbEnvIdx >= 0) {
|
||||
envVars[dbEnvIdx] = { name: "OPENCODE_DB", value: "/opencode-db" };
|
||||
} else {
|
||||
envVars.push({ name: "OPENCODE_DB", value: "/opencode-db" });
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime config for permissions
|
||||
@@ -360,9 +361,13 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
volumeMounts.push({ name: "data", mountPath: "/paperclip" });
|
||||
}
|
||||
|
||||
// Ephemeral DB volume (only for ephemeral mode)
|
||||
if (opencodeDbMode === "ephemeral") {
|
||||
volumes.push({ name: "opencode-db", emptyDir: {} });
|
||||
// OpenCode DB volume: dedicated PVC (string claim name) or ephemeral emptyDir (null)
|
||||
if (input.agentDbClaimName !== undefined) {
|
||||
volumes.push(
|
||||
input.agentDbClaimName !== null
|
||||
? { name: "opencode-db", persistentVolumeClaim: { claimName: input.agentDbClaimName } }
|
||||
: { name: "opencode-db", emptyDir: {} },
|
||||
);
|
||||
volumeMounts.push({ name: "opencode-db", mountPath: "/opencode-db" });
|
||||
}
|
||||
|
||||
@@ -395,25 +400,6 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
fsGroupChangePolicy: "OnRootMismatch",
|
||||
};
|
||||
|
||||
// Build init container pieces
|
||||
// shared_pvc mode: prepend mkdir -p to guarantee the per-agent DB dir exists before opencode starts
|
||||
const useSharedPvcMkdir = opencodeDbMode === "shared_pvc" && Boolean(selfPod.pvcClaimName);
|
||||
const dbSetupPrefix = useSharedPvcMkdir ? `mkdir -p "$OPENCODE_DB_PATH" && ` : "";
|
||||
const initVolumeMounts: k8s.V1VolumeMount[] = [{ name: "prompt", mountPath: "/tmp/prompt" }];
|
||||
const initEnvVars: k8s.V1EnvVar[] = [];
|
||||
if (input.promptSecretName) {
|
||||
initVolumeMounts.push({ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true });
|
||||
} else {
|
||||
initEnvVars.push({ name: "PROMPT_CONTENT", value: prompt });
|
||||
}
|
||||
if (useSharedPvcMkdir) {
|
||||
initVolumeMounts.push({ name: "data", mountPath: "/paperclip" });
|
||||
initEnvVars.push({ name: "OPENCODE_DB_PATH", value: resolvedDbPath });
|
||||
}
|
||||
const initContainerCommand = input.promptSecretName
|
||||
? `${dbSetupPrefix}cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt`
|
||||
: `${dbSetupPrefix}printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`;
|
||||
|
||||
// Build the main container command
|
||||
// 1. Optionally write opencode runtime config for permission bypass
|
||||
// 2. Pipe prompt into opencode
|
||||
@@ -454,9 +440,19 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
name: "write-prompt",
|
||||
image: "busybox:1.36",
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["sh", "-c", initContainerCommand],
|
||||
...(initEnvVars.length > 0 ? { env: initEnvVars } : {}),
|
||||
volumeMounts: initVolumeMounts,
|
||||
...(input.promptSecretName
|
||||
? {
|
||||
command: ["sh", "-c", "cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt"],
|
||||
volumeMounts: [
|
||||
{ name: "prompt", mountPath: "/tmp/prompt" },
|
||||
{ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true },
|
||||
],
|
||||
}
|
||||
: {
|
||||
command: ["sh", "-c", "printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"],
|
||||
env: [{ name: "PROMPT_CONTENT", value: prompt }],
|
||||
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
|
||||
}),
|
||||
securityContext,
|
||||
resources: {
|
||||
requests: { cpu: "10m", memory: "16Mi" },
|
||||
|
||||
Reference in New Issue
Block a user