192 lines
7.8 KiB
TypeScript
192 lines
7.8 KiB
TypeScript
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 SecretRef;runner 将按 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";
|
||
}
|