feat: 补充 runner Job dry-run 骨架
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user