Files
pikasTech-agentrun/src/runner/k8s-job.ts
T
2026-05-29 11:45:30 +08:00

192 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { stableHash } from "../common/validation.js";
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, SecretRef } from "../common/types.js";
export interface RunnerJobRenderOptions {
run: RunRecord;
commandId: string;
managerUrl: string;
image: string;
namespace?: string;
attemptId?: string;
runnerId?: string;
sourceCommit?: string;
serviceAccountName?: string;
imagePullPolicy?: string;
backoffLimit?: number;
ttlSecondsAfterFinished?: number;
}
interface CredentialProjection {
profile: BackendProfile | string;
secretRef: SecretRef;
volumeName: string;
mountPath: string;
}
export function renderRunnerJobDryRun(options: RunnerJobRenderOptions): JsonRecord {
const render = renderRunnerJobManifest(options);
return {
dryRun: true,
mutation: false,
action: "render-kubernetes-job",
jobIdentity: {
kind: "Job",
namespace: render.namespace,
name: render.jobName,
serviceAccountName: render.serviceAccountName,
},
runner: {
runId: options.run.id,
commandId: options.commandId,
attemptId: render.attemptId,
runnerId: render.runnerId,
backendProfile: options.run.backendProfile,
managerUrl: options.managerUrl,
sourceCommit: render.sourceCommit,
},
secretRefs: render.secretRefs.map((item) => ({ profile: item.profile, name: item.secretRef.name, namespace: item.secretRef.namespace ?? render.namespace, keys: item.secretRef.keys ?? [], mountPath: item.mountPath, valuesPrinted: false })),
pollCommands: {
run: `bun scripts/agentrun-cli.ts runs show ${options.run.id} --manager-url ${options.managerUrl}`,
events: `bun scripts/agentrun-cli.ts runs events ${options.run.id} --manager-url ${options.managerUrl} --after-seq 0 --limit 100`,
},
warnings: render.warnings,
manifest: render.manifest,
};
}
export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { manifest: JsonRecord; namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; serviceAccountName: string; secretRefs: CredentialProjection[]; warnings: string[] } {
const namespace = options.namespace ?? "agentrun-v01";
const attemptId = options.attemptId ?? `attempt_${Date.now().toString(36)}`;
const runnerId = options.runnerId ?? `runner_${shortHash(`${options.run.id}:${attemptId}:${options.commandId}`)}`;
const sourceCommit = options.sourceCommit ?? process.env.AGENTRUN_SOURCE_COMMIT ?? "unknown";
const serviceAccountName = options.serviceAccountName ?? "agentrun-v01-runner";
const jobName = `agentrun-v01-runner-${shortDnsHash(options.run.id, attemptId)}`;
const secretRefs = credentialProjections(options.run, namespace);
const warnings: string[] = [];
if (secretRefs.length === 0) warnings.push("run executionPolicy.secretScope 未声明 provider SecretRefrunner 将按 secret-unavailable 上报,而不会降级直连外部凭据");
const env = runnerEnv(options, { namespace, jobName, runnerId, attemptId, sourceCommit, secretRefs });
const manifest: JsonRecord = {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name: jobName,
namespace,
labels: labels(options.run, jobName),
annotations: {
"agentrun.pikastech.local/run-id": options.run.id,
"agentrun.pikastech.local/command-id": options.commandId,
"agentrun.pikastech.local/dry-run-render": "true",
},
},
spec: {
backoffLimit: options.backoffLimit ?? 0,
ttlSecondsAfterFinished: options.ttlSecondsAfterFinished ?? 86_400,
template: {
metadata: {
labels: labels(options.run, jobName),
annotations: {
"agentrun.pikastech.local/run-id": options.run.id,
"agentrun.pikastech.local/command-id": options.commandId,
},
},
spec: {
serviceAccountName,
automountServiceAccountToken: false,
restartPolicy: "Never",
containers: [
{
name: "runner",
image: options.image,
imagePullPolicy: options.imagePullPolicy ?? "IfNotPresent",
command: ["bun", "src/runner/main.ts"],
env,
volumeMounts: secretRefs.map((item) => ({ name: item.volumeName, mountPath: item.mountPath, readOnly: true })),
resources: {
requests: { cpu: "250m", memory: "512Mi" },
limits: { cpu: "2", memory: "4Gi" },
},
securityContext: {
allowPrivilegeEscalation: false,
readOnlyRootFilesystem: false,
capabilities: { drop: ["ALL"] },
},
},
],
volumes: secretRefs.map(secretVolume),
},
},
},
};
return { manifest, namespace, jobName, runnerId, attemptId, sourceCommit, serviceAccountName, secretRefs, warnings };
}
function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[] }): JsonRecord[] {
const codexMount = context.secretRefs.find((item) => item.profile === "codex")?.mountPath ?? "/home/agentrun/.codex";
return [
{ name: "AGENTRUN_MGR_URL", value: options.managerUrl },
{ name: "AGENTRUN_RUN_ID", value: options.run.id },
{ name: "AGENTRUN_COMMAND_ID", value: options.commandId },
{ name: "AGENTRUN_ATTEMPT_ID", value: context.attemptId },
{ name: "AGENTRUN_RUNNER_ID", value: context.runnerId },
{ name: "AGENTRUN_BACKEND_PROFILE", value: options.run.backendProfile },
{ name: "AGENTRUN_EXECUTION_POLICY_JSON", value: JSON.stringify(options.run.executionPolicy) },
{ name: "AGENTRUN_SOURCE_COMMIT", value: context.sourceCommit },
{ name: "AGENTRUN_RUNTIME_NAMESPACE", value: context.namespace },
{ name: "AGENTRUN_K8S_JOB_NAME", value: context.jobName },
{ name: "AGENTRUN_LOG_PATH", value: "/tmp/agentrun-runner.jsonl" },
{ name: "HOME", value: "/home/agentrun" },
{ name: "CODEX_HOME", value: codexMount },
];
}
function credentialProjections(run: RunRecord, namespace: string): CredentialProjection[] {
const policy: ExecutionPolicy = run.executionPolicy;
const credentials = policy.secretScope.providerCredentials ?? [];
return credentials.map((item, index) => ({
profile: item.profile,
secretRef: item.secretRef.namespace ? item.secretRef : { ...item.secretRef, namespace },
volumeName: sanitizeVolumeName(`${String(item.profile)}-${index}`),
mountPath: normalizeMountPath(item.secretRef.mountPath),
}));
}
function secretVolume(item: CredentialProjection): JsonRecord {
const secret: JsonRecord = {
secretName: item.secretRef.name,
defaultMode: 256,
};
const keys = item.secretRef.keys ?? [];
if (keys.length > 0) secret.items = keys.map((key) => ({ key, path: key, mode: 256 }));
return { name: item.volumeName, secret };
}
function normalizeMountPath(value: string | undefined): string {
if (!value || value === "~/.codex") return "/home/agentrun/.codex";
if (value.startsWith("~/")) return `/home/agentrun/${value.slice(2)}`;
return value;
}
function labels(run: RunRecord, jobName: string): JsonRecord {
return {
"app.kubernetes.io/name": "agentrun-runner",
"app.kubernetes.io/component": "runner",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": "v0.1",
"agentrun.pikastech.local/run-hash": shortHash(run.id),
"job-name": jobName,
};
}
function shortDnsHash(...parts: string[]): string {
return shortHash(parts.join(":"));
}
function shortHash(value: JsonValue): string {
return stableHash(value).slice(0, 12);
}
function sanitizeVolumeName(value: string): string {
const sanitized = value.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "");
return sanitized.length > 0 ? sanitized.slice(0, 40) : "credential";
}